diff --git a/ui/index.html b/ui/index.html index 3409ddbf877..dc03f49115c 100644 --- a/ui/index.html +++ b/ui/index.html @@ -8,18 +8,6 @@ - diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 5c6cdcff7b3..a54f31e583a 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -12,7 +12,6 @@ export const en: TranslationMap = { na: "n/a", docs: "Docs", resources: "Resources", - search: "Search", }, nav: { chat: "Chat", @@ -105,47 +104,6 @@ export const en: TranslationMap = { hint: "This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or open {url} on the gateway host.", stayHttp: "If you must stay on HTTP, set {config} (token-only).", }, - connection: { - title: "How to connect", - step1: "Start the gateway on your host machine:", - step2: "Get a tokenized dashboard URL:", - step3: "Paste the WebSocket URL and token above, or open the tokenized URL directly.", - step4: "Or generate a reusable token:", - docsHint: "For remote access, Tailscale Serve is recommended. ", - docsLink: "Read the docs →", - }, - cards: { - cost: "Cost", - skills: "Skills", - recentSessions: "Recent Sessions", - }, - attention: { - title: "Attention", - }, - eventLog: { - title: "Event Log", - }, - logTail: { - title: "Gateway Logs", - }, - quickActions: { - newSession: "New Session", - automation: "Automation", - refreshAll: "Refresh All", - terminal: "Terminal", - }, - streamMode: { - active: "Stream mode — values redacted", - disable: "Disable", - }, - palette: { - placeholder: "Type a command…", - noResults: "No results", - }, - }, - login: { - subtitle: "Gateway Dashboard", - passwordPlaceholder: "optional", }, chat: { disconnected: "Disconnected from gateway.", diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index 9d848e2a183..6c34f2317bf 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -12,7 +12,6 @@ export const pt_BR: TranslationMap = { na: "n/a", docs: "Docs", resources: "Recursos", - search: "Pesquisar", }, nav: { chat: "Chat", @@ -107,47 +106,6 @@ export const pt_BR: TranslationMap = { hint: "Esta página é HTTP, então o navegador bloqueia a identidade do dispositivo. Use HTTPS (Tailscale Serve) ou abra {url} no host do gateway.", stayHttp: "Se você precisar permanecer em HTTP, defina {config} (apenas token).", }, - connection: { - title: "Como conectar", - step1: "Inicie o gateway na sua máquina host:", - step2: "Obtenha uma URL do painel com token:", - step3: "Cole a URL do WebSocket e o token acima, ou abra a URL com token diretamente.", - step4: "Ou gere um token reutilizável:", - docsHint: "Para acesso remoto, recomendamos o Tailscale Serve. ", - docsLink: "Leia a documentação →", - }, - cards: { - cost: "Custo", - skills: "Habilidades", - recentSessions: "Sessões Recentes", - }, - attention: { - title: "Atenção", - }, - eventLog: { - title: "Log de Eventos", - }, - logTail: { - title: "Logs do Gateway", - }, - quickActions: { - newSession: "Nova Sessão", - automation: "Automação", - refreshAll: "Atualizar Tudo", - terminal: "Terminal", - }, - streamMode: { - active: "Modo stream — valores ocultos", - disable: "Desativar", - }, - palette: { - placeholder: "Digite um comando…", - noResults: "Sem resultados", - }, - }, - login: { - subtitle: "Painel do Gateway", - passwordPlaceholder: "opcional", }, chat: { disconnected: "Desconectado do gateway.", diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index 566a9cd0d4c..e757b0ef8f9 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -12,7 +12,6 @@ export const zh_CN: TranslationMap = { na: "不适用", docs: "文档", resources: "资源", - search: "搜索", }, nav: { chat: "聊天", @@ -104,47 +103,6 @@ export const zh_CN: TranslationMap = { hint: "此页面为 HTTP,因此浏览器阻止设备标识。请使用 HTTPS (Tailscale Serve) 或在网关主机上打开 {url}。", stayHttp: "如果您必须保持 HTTP,请设置 {config} (仅限令牌)。", }, - connection: { - title: "如何连接", - step1: "在主机上启动网关:", - step2: "获取带令牌的仪表盘 URL:", - step3: "将 WebSocket URL 和令牌粘贴到上方,或直接打开带令牌的 URL。", - step4: "或生成可重复使用的令牌:", - docsHint: "如需远程访问,建议使用 Tailscale Serve。", - docsLink: "查看文档 →", - }, - cards: { - cost: "费用", - skills: "技能", - recentSessions: "最近会话", - }, - attention: { - title: "注意事项", - }, - eventLog: { - title: "事件日志", - }, - logTail: { - title: "网关日志", - }, - quickActions: { - newSession: "新建会话", - automation: "自动化", - refreshAll: "全部刷新", - terminal: "终端", - }, - streamMode: { - active: "流模式 — 数据已隐藏", - disable: "禁用", - }, - palette: { - placeholder: "输入命令…", - noResults: "无结果", - }, - }, - login: { - subtitle: "网关仪表盘", - passwordPlaceholder: "可选", }, chat: { disconnected: "已断开与网关的连接。", diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index 5fa32aa31c1..d0d8e141f27 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -12,7 +12,6 @@ export const zh_TW: TranslationMap = { na: "不適用", docs: "文檔", resources: "資源", - search: "搜尋", }, nav: { chat: "聊天", @@ -104,47 +103,6 @@ export const zh_TW: TranslationMap = { hint: "此頁面為 HTTP,因此瀏覽器阻止設備標識。請使用 HTTPS (Tailscale Serve) 或在網關主機上打開 {url}。", stayHttp: "如果您必須保持 HTTP,請設置 {config} (僅限令牌)。", }, - connection: { - title: "如何連接", - step1: "在主機上啟動閘道:", - step2: "取得帶令牌的儀表板 URL:", - step3: "將 WebSocket URL 和令牌貼到上方,或直接開啟帶令牌的 URL。", - step4: "或產生可重複使用的令牌:", - docsHint: "如需遠端存取,建議使用 Tailscale Serve。", - docsLink: "查看文件 →", - }, - cards: { - cost: "費用", - skills: "技能", - recentSessions: "最近會話", - }, - attention: { - title: "注意事項", - }, - eventLog: { - title: "事件日誌", - }, - logTail: { - title: "閘道日誌", - }, - quickActions: { - newSession: "新建會話", - automation: "自動化", - refreshAll: "全部刷新", - terminal: "終端", - }, - streamMode: { - active: "串流模式 — 數據已隱藏", - disable: "禁用", - }, - palette: { - placeholder: "輸入指令…", - noResults: "無結果", - }, - }, - login: { - subtitle: "閘道儀表板", - passwordPlaceholder: "可選", }, chat: { disconnected: "已斷開與網關的連接。", diff --git a/ui/src/styles.css b/ui/src/styles.css index 7eb2fd17046..16b327f3a73 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -2,5 +2,4 @@ @import "./styles/layout.css"; @import "./styles/layout.mobile.css"; @import "./styles/components.css"; -@import "./styles/glass.css"; @import "./styles/config.css"; diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index 01f9fb3e641..b83afd32c50 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -1,500 +1,108 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@400;700&family=JetBrains+Mono:wght@400;500;700&display=swap"); - -* { - box-sizing: border-box; -} - -/* ════════════════════════════════════════════════════════ - Theme System — 6 Glassmorphism Themes - ════════════════════════════════════════════════════════ */ - -/* ─── Design Tokens (shared across all themes) ─── */ +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"); :root { - --icon-size-xs: 0.9rem; - --icon-size-sm: 1.05rem; - --icon-size-md: 1.25rem; - --icon-size-xl: 2.4rem; + /* Background - Warmer dark with depth */ + --bg: #12141a; + --bg-accent: #14161d; + --bg-elevated: #1a1d25; + --bg-hover: #262a35; + --bg-muted: #262a35; - --font-inter: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - --font-serif: "Playfair Display", Georgia, "Times New Roman", serif; - --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + /* Card / Surface - More contrast between levels */ + --card: #181b22; + --card-foreground: #f4f4f5; + --card-highlight: rgba(255, 255, 255, 0.05); + --popover: #181b22; + --popover-foreground: #f4f4f5; - --theme-switch-x: 50%; - --theme-switch-y: 50%; -} + /* Panel */ + --panel: #12141a; + --panel-strong: #1a1d25; + --panel-hover: #262a35; + --chrome: rgba(18, 20, 26, 0.95); + --chrome-strong: rgba(18, 20, 26, 0.98); -@media (prefers-reduced-motion: reduce) { - :root { - --clay-duration-fast: 0ms; - --clay-duration-normal: 0ms; - --clay-duration-slow: 0ms; - } + /* Text - Slightly warmer */ + --text: #e4e4e7; + --text-strong: #fafafa; + --chat-text: #e4e4e7; + --muted: #71717a; + --muted-strong: #52525b; + --muted-foreground: #71717a; - * { - animation-duration: 0s !important; - transition-duration: 0s !important; - } -} + /* Border - Subtle but defined */ + --border: #27272a; + --border-strong: #3f3f46; + --border-hover: #52525b; + --input: #27272a; + --ring: #ff5c5c; -/* ─── Theme: dark (Home) — Deep-sea Operations Console ─── */ - -:root, -:root[data-theme="dark"] { - color-scheme: dark; - - --vscode-bg: #040810; - --vscode-sidebar: #06090f; - --vscode-panel: #0a0e16; - --vscode-panel-border: rgba(0, 212, 170, 0.08); - --vscode-surface: #0e1420; - --vscode-hover: #121a28; - --vscode-contrast: #020408; - --vscode-text: #d0d8e4; - --vscode-muted: #6e7a8a; - --vscode-subtle: #3a4454; - --vscode-ghost: #0c1018; - --vscode-accent: #ca3a29; - --vscode-accent-alpha: rgba(202, 58, 41, 0.14); - --vscode-selection: #3d1418; - --vscode-success: #00d4aa; - --vscode-danger: #ca3a29; - - --kn-claw: #ca3a29; - --kn-claw-bright: #fd8e2e; - --kn-claw-dim: rgba(202, 58, 41, 0.12); - --kn-claw-ember: #fb9231; - --kn-claw-deep: #9a2d1f; - --kn-ocean: #09181e; - --kn-ocean-bright: #132a36; - --kn-ocean-mid: #0c1e28; - --kn-ocean-dim: rgba(9, 24, 30, 0.8); - --kn-ocean-deep: #040810; - --kn-silver: #8a9baa; - --kn-silver-bright: #c0cdd6; - --kn-silver-dim: rgba(138, 155, 170, 0.12); - --kn-bioluminescence: #00d4aa; - --kn-warm-dark: #221016; - --kn-void: #221016; - - --glass-blur: 8px; - --glass-saturate: 120%; - --glass-bg: rgba(10, 14, 22, 0.82); - --glass-bg-elevated: rgba(14, 20, 32, 0.88); - --glass-border: rgba(0, 212, 170, 0.08); - --glass-border-hover: rgba(202, 58, 41, 0.3); - --glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.04); - --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4); - --glass-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 212, 170, 0.06); - --glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 212, 170, 0.08); - - --radius-xs: 4px; - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - --radius-full: 9999px; -} - -/* ─── Theme: light (Docs) — Warm Editorial Dark ─── */ - -:root[data-theme="light"] { - color-scheme: dark; - - --vscode-bg: #0e0c0e; - --vscode-sidebar: #131012; - --vscode-panel: #161214; - --vscode-panel-border: rgba(255, 255, 255, 0.06); - --vscode-surface: #1a1618; - --vscode-hover: #201c1e; - --vscode-contrast: #080608; - --vscode-text: #d5d0cf; - --vscode-muted: #7a7472; - --vscode-subtle: #4a4442; - --vscode-ghost: #1a1616; - --vscode-accent: #ca3a29; - --vscode-accent-alpha: rgba(202, 58, 41, 0.14); - --vscode-selection: #3d1418; - --vscode-success: #00d4aa; - --vscode-danger: #ca3a29; - - --kn-claw: #ca3a29; - --kn-claw-bright: #fd8e2e; - --kn-claw-dim: rgba(202, 58, 41, 0.12); - --kn-claw-ember: #fb9231; - --kn-claw-deep: #9a2d1f; - --kn-ocean: #0e0c0e; - --kn-ocean-bright: #201c1e; - --kn-ocean-mid: #161214; - --kn-ocean-dim: rgba(14, 12, 14, 0.8); - --kn-ocean-deep: #0e0c0e; - --kn-silver: #8a7e72; - --kn-silver-bright: #c0b4a8; - --kn-silver-dim: rgba(138, 126, 114, 0.12); - --kn-bioluminescence: #00d4aa; - --kn-warm-dark: #1a1416; - --kn-void: #1a1416; - - --glass-blur: 0px; - --glass-saturate: 100%; - --glass-bg: rgba(22, 18, 20, 0.95); - --glass-bg-elevated: rgba(26, 22, 24, 0.96); - --glass-border: rgba(255, 255, 255, 0.06); - --glass-border-hover: rgba(202, 58, 41, 0.25); - --glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.03); - --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); - --glass-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4); - --glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5); - - --radius-xs: 4px; - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - --radius-full: 9999px; -} - -/* ─── Theme: openknot — Minimalist Premium Noir ─── */ - -:root[data-theme="openknot"] { - color-scheme: dark; - - --vscode-bg: #000000; - --vscode-sidebar: #080808; - --vscode-panel: #0c0c0c; - --vscode-panel-border: rgba(167, 139, 250, 0.08); - --vscode-surface: #111111; - --vscode-hover: #181818; - --vscode-contrast: #000000; - --vscode-text: #e4e4e7; - --vscode-muted: #71717a; - --vscode-subtle: #3f3f46; - --vscode-ghost: #18181b; - --vscode-accent: #a78bfa; - --vscode-accent-alpha: rgba(167, 139, 250, 0.14); - --vscode-selection: #2e1a5e; - --vscode-success: #a78bfa; - --vscode-danger: #a78bfa; - - --kn-claw: #a78bfa; - --kn-claw-bright: #c4b5fd; - --kn-claw-dim: rgba(167, 139, 250, 0.12); - --kn-claw-ember: #c4b5fd; - --kn-claw-deep: #7c3aed; - --kn-ocean: #000000; - --kn-ocean-bright: #1a1a1e; - --kn-ocean-mid: #0e0e12; - --kn-ocean-dim: rgba(0, 0, 0, 0.8); - --kn-ocean-deep: #000000; - --kn-silver: #71717a; - --kn-silver-bright: #a1a1aa; - --kn-silver-dim: rgba(113, 113, 122, 0.12); - --kn-bioluminescence: #c4b5fd; - --kn-warm-dark: #18181b; - --kn-void: #18181b; - - --glass-blur: 12px; - --glass-saturate: 110%; - --glass-bg: rgba(12, 12, 12, 0.85); - --glass-bg-elevated: rgba(17, 17, 17, 0.9); - --glass-border: rgba(167, 139, 250, 0.08); - --glass-border-hover: rgba(167, 139, 250, 0.3); - --glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.04); - --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.5); - --glass-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(167, 139, 250, 0.06); - --glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(167, 139, 250, 0.08); - - --radius-xs: 4px; - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - --radius-full: 9999px; -} - -/* ─── Theme: fieldmanual — Industrial Dossier ─── */ - -:root[data-theme="fieldmanual"] { - color-scheme: dark; - - --vscode-bg: #0e0e0e; - --vscode-sidebar: #121212; - --vscode-panel: #161616; - --vscode-panel-border: rgba(255, 255, 255, 0.1); - --vscode-surface: #1a1a1a; - --vscode-hover: #222222; - --vscode-contrast: #0a0a0a; - --vscode-text: #d4d4d4; - --vscode-muted: #737373; - --vscode-subtle: #404040; - --vscode-ghost: #1a1a1a; - --vscode-accent: #ca3a29; - --vscode-accent-alpha: rgba(202, 58, 41, 0.14); - --vscode-selection: #3d1418; - --vscode-success: #61d6ff; - --vscode-danger: #ca3a29; - - --kn-claw: #ca3a29; - --kn-claw-bright: #ff6b4a; - --kn-claw-dim: rgba(202, 58, 41, 0.12); - --kn-claw-ember: #ff6b4a; - --kn-claw-deep: #9a2d1f; - --kn-ocean: #0e0e0e; - --kn-ocean-bright: #222222; - --kn-ocean-mid: #161616; - --kn-ocean-dim: rgba(14, 14, 14, 0.8); - --kn-ocean-deep: #0e0e0e; - --kn-silver: #737373; - --kn-silver-bright: #a3a3a3; - --kn-silver-dim: rgba(115, 115, 115, 0.12); - --kn-bioluminescence: #61d6ff; - --kn-warm-dark: #1a1a1a; - --kn-void: #1a1a1a; - - --glass-blur: 0px; - --glass-saturate: 100%; - --glass-bg: rgba(22, 22, 22, 0.95); - --glass-bg-elevated: rgba(26, 26, 26, 0.96); - --glass-border: rgba(255, 255, 255, 0.1); - --glass-border-hover: rgba(202, 58, 41, 0.35); - --glass-highlight: none; - --glass-shadow-sm: none; - --glass-shadow-md: none; - --glass-shadow-lg: none; - - --radius-xs: 0px; - --radius-sm: 0px; - --radius-md: 0px; - --radius-lg: 0px; - --radius-xl: 0px; - --radius-full: 0px; -} - -/* ─── Theme: openai — Crimson Glassmorphic ─── */ - -:root[data-theme="openai"] { - color-scheme: dark; - - --vscode-bg: #0c0606; - --vscode-sidebar: #100808; - --vscode-panel: #140a0a; - --vscode-panel-border: rgba(202, 58, 41, 0.12); - --vscode-surface: #1a0e0e; - --vscode-hover: #221414; - --vscode-contrast: #060202; - --vscode-text: #e8d8d4; - --vscode-muted: #8a6a64; - --vscode-subtle: #4a3430; - --vscode-ghost: #1a0e0e; - --vscode-accent: #ca3a29; - --vscode-accent-alpha: rgba(202, 58, 41, 0.18); - --vscode-selection: #7d261c; - --vscode-success: #fd8e2e; - --vscode-danger: #ca3a29; - - --kn-claw: #ca3a29; - --kn-claw-bright: #ff4e41; - --kn-claw-dim: rgba(202, 58, 41, 0.15); - --kn-claw-ember: #fd8e2e; - --kn-claw-deep: #9a2d1f; - --kn-ocean: #0c0606; - --kn-ocean-bright: #221414; - --kn-ocean-mid: #140a0a; - --kn-ocean-dim: rgba(12, 6, 6, 0.8); - --kn-ocean-deep: #0c0606; - --kn-silver: #8a6a64; - --kn-silver-bright: #c0a49c; - --kn-silver-dim: rgba(138, 106, 100, 0.12); - --kn-bioluminescence: #fd8e2e; - --kn-warm-dark: #221016; - --kn-void: #221016; - - --glass-blur: 14px; - --glass-saturate: 130%; - --glass-bg: rgba(20, 10, 10, 0.78); - --glass-bg-elevated: rgba(26, 14, 14, 0.85); - --glass-border: rgba(202, 58, 41, 0.12); - --glass-border-hover: rgba(202, 58, 41, 0.4); - --glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.05); - --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 4px rgba(202, 58, 41, 0.08); - --glass-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 12px rgba(202, 58, 41, 0.1); - --glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6), 0 0 24px rgba(202, 58, 41, 0.12); - - --radius-xs: 4px; - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - --radius-full: 9999px; -} - -/* ─── Theme: clawdash — Chrome Metallic ─── */ - -:root[data-theme="clawdash"] { - color-scheme: dark; - - --vscode-bg: #050507; - --vscode-sidebar: #08080c; - --vscode-panel: #0c0c10; - --vscode-panel-border: rgba(192, 200, 212, 0.1); - --vscode-surface: #101014; - --vscode-hover: #161620; - --vscode-contrast: #020204; - --vscode-text: #e8ecf0; - --vscode-muted: #8a94a4; - --vscode-subtle: #4a5060; - --vscode-ghost: #1a1a22; - --vscode-accent: #ca3a29; - --vscode-accent-alpha: rgba(202, 58, 41, 0.14); - --vscode-selection: #3d1418; - --vscode-success: #00d4aa; - --vscode-danger: #ca3a29; - - --kn-claw: #ca3a29; - --kn-claw-bright: #ff4e41; - --kn-claw-dim: rgba(202, 58, 41, 0.12); - --kn-claw-ember: #fd8e2e; - --kn-claw-deep: #9a2d1f; - --kn-ocean: #08080c; - --kn-ocean-bright: #161620; - --kn-ocean-mid: #0c0c10; - --kn-ocean-dim: rgba(8, 8, 12, 0.8); - --kn-ocean-deep: #050507; - --kn-silver: #7a8494; - --kn-silver-bright: #c0c8d4; - --kn-silver-dim: rgba(192, 200, 212, 0.12); - --kn-bioluminescence: #00d4aa; - --kn-warm-dark: #1a1a22; - --kn-void: #1a1a22; - - --glass-blur: 16px; - --glass-saturate: 150%; - --glass-bg: rgba(12, 12, 16, 0.8); - --glass-bg-elevated: rgba(16, 16, 20, 0.88); - --glass-border: rgba(192, 200, 212, 0.08); - --glass-border-hover: rgba(192, 200, 212, 0.25); - --glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06); - --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 4px rgba(192, 200, 212, 0.04); - --glass-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 12px rgba(192, 200, 212, 0.06); - --glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6), 0 0 24px rgba(192, 200, 212, 0.08); - - --radius-xs: 3px; - --radius-sm: 6px; - --radius-md: 8px; - --radius-lg: 10px; - --radius-xl: 14px; - --radius-full: 9999px; -} - -/* ─── Semantic Alias Layer ─── - Maps foundation vars to the short names used throughout - component CSS, so themes work without per-component overrides. */ - -:root, -:root[data-theme="dark"], -:root[data-theme="light"], -:root[data-theme="openknot"], -:root[data-theme="fieldmanual"], -:root[data-theme="openai"], -:root[data-theme="clawdash"] { - /* Core surfaces */ - --bg: var(--vscode-bg); - --bg-accent: var(--vscode-sidebar); - --bg-elevated: var(--vscode-surface); - --bg-hover: var(--vscode-hover); - --bg-muted: var(--vscode-sidebar); - --bg-content: var(--vscode-bg); - - /* Card/popover surfaces */ - --card: var(--vscode-panel); - --card-foreground: var(--vscode-text); - --card-highlight: rgba(255, 255, 255, 0.04); - --popover: var(--vscode-panel); - --popover-foreground: var(--vscode-text); - - /* Panel/chrome surfaces */ - --panel: var(--vscode-sidebar); - --panel-strong: var(--vscode-panel); - --panel-hover: var(--vscode-hover); - --chrome: var(--glass-bg); - --chrome-strong: var(--glass-bg-elevated); - - /* Typography */ - --text: var(--vscode-text); - --text-strong: var(--vscode-text); - --chat-text: var(--vscode-text); - --muted: var(--vscode-muted); - --muted-strong: var(--vscode-subtle); - --muted-foreground: var(--vscode-muted); - - /* Borders + controls */ - --border: var(--glass-border); - --border-strong: var(--glass-border-hover); - --border-hover: var(--glass-border-hover); - --input: var(--glass-border); - --ring: var(--vscode-accent); - - /* Accent */ - --accent: var(--vscode-accent); - --accent-strong: var(--kn-claw-deep); - --accent-hover: var(--kn-claw-bright); - --accent-muted: var(--vscode-accent); - --accent-subtle: var(--vscode-accent-alpha); + /* Accent - Punchy signature red */ + --accent: #ff5c5c; + --accent-hover: #ff7070; + --accent-muted: #ff5c5c; + --accent-subtle: rgba(255, 92, 92, 0.15); --accent-foreground: #fafafa; - --accent-glow: var(--kn-claw-dim); - --accent-soft: var(--vscode-accent-alpha); - --primary: var(--vscode-accent); + --accent-glow: rgba(255, 92, 92, 0.25); + --primary: #ff5c5c; --primary-foreground: #ffffff; - /* Secondary */ - --secondary: var(--vscode-sidebar); - --secondary-foreground: var(--vscode-text); - --accent-2: var(--kn-bioluminescence); - --accent-2-muted: var(--kn-silver); - --accent-2-subtle: var(--kn-silver-dim); + /* Secondary - Teal accent for variety */ + --secondary: #1e2028; + --secondary-foreground: #f4f4f5; + --accent-2: #14b8a6; + --accent-2-muted: rgba(20, 184, 166, 0.7); + --accent-2-subtle: rgba(20, 184, 166, 0.15); - /* Semantic */ - --ok: var(--vscode-success); - --ok-muted: var(--vscode-success); - --ok-subtle: var(--kn-silver-dim); - --destructive: var(--vscode-danger); + /* Semantic - More saturated */ + --ok: #22c55e; + --ok-muted: rgba(34, 197, 94, 0.75); + --ok-subtle: rgba(34, 197, 94, 0.12); + --destructive: #ef4444; --destructive-foreground: #fafafa; - --warn: var(--kn-claw-ember); - --warn-muted: var(--kn-claw-ember); - --warn-subtle: var(--kn-claw-dim); - --danger: var(--vscode-danger); - --danger-muted: var(--vscode-danger); - --danger-subtle: var(--kn-claw-dim); + --warn: #f59e0b; + --warn-muted: rgba(245, 158, 11, 0.75); + --warn-subtle: rgba(245, 158, 11, 0.12); + --danger: #ef4444; + --danger-muted: rgba(239, 68, 68, 0.75); + --danger-subtle: rgba(239, 68, 68, 0.12); --info: #3b82f6; - --success: var(--vscode-success); - /* Focus */ - --focus: var(--kn-claw-dim); - --focus-offset-color: var(--bg); - --focus-ring-width: 2px; - --focus-ring-offset-width: 2px; - --focus-ring-color: var(--vscode-accent); - --focus-ring: - 0 0 0 var(--focus-ring-offset-width) var(--focus-offset-color), - 0 0 0 calc(var(--focus-ring-offset-width) + var(--focus-ring-width)) var(--focus-ring-color); - --focus-glow: - 0 0 0 var(--focus-ring-offset-width) var(--focus-offset-color), - 0 0 0 calc(var(--focus-ring-offset-width) + var(--focus-ring-width)) var(--focus-ring-color), - 0 0 18px var(--accent-glow); + /* Focus - With glow */ + --focus: rgba(255, 92, 92, 0.25); + --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring); + --focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 20px var(--accent-glow); + /* Grid */ --grid-line: rgba(255, 255, 255, 0.04); - /* Shadows */ - --shadow-sm: var(--glass-shadow-sm); - --shadow-md: var(--glass-shadow-md); - --shadow-lg: var(--glass-shadow-lg); - --shadow-xl: var(--glass-shadow-lg); + /* Theme transition */ + --theme-switch-x: 50%; + --theme-switch-y: 50%; + + /* Typography - Space Grotesk for personality */ + --mono: + "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; + --font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-display: + "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Shadows - Richer with subtle color */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.03); + --shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.03); + --shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.03); --shadow-glow: 0 0 30px var(--accent-glow); - /* Radii — aliased from foundation */ - --radius: var(--radius-md); + /* Radii - Slightly larger for friendlier feel */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + --radius: 8px; - /* Timing */ + /* Transitions - Snappy but smooth */ --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); @@ -502,68 +110,88 @@ --duration-normal: 200ms; --duration-slow: 350ms; - /* Typography stacks */ - --mono: - "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; - --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - --font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - - /* Clay compat layer (dashboard-lit components) */ - --clay-bg: var(--vscode-bg); - --clay-bg-card: var(--vscode-panel); - --clay-bg-elevated: var(--vscode-surface); - --clay-bg-button: var(--vscode-hover); - --clay-bg-interactive: var(--vscode-accent-alpha); - --clay-bg-pressed: var(--vscode-selection); - --clay-bg-scrim: rgba(0, 0, 0, 0.6); - --clay-border-color: var(--glass-border); - --clay-border-subtle: var(--vscode-panel-border); - --clay-shadow: var(--glass-shadow-sm); - --clay-shadow-elevated: var(--glass-shadow-md); - --clay-shadow-pressed: var(--glass-shadow-sm); - --clay-shadow-subtle: var(--glass-shadow-sm); - --clay-radius-sm: var(--radius-sm); - --clay-radius: var(--radius-md); - --clay-radius-md: var(--radius-md); - --clay-radius-lg: var(--radius-lg); - --clay-radius-xl: var(--radius-xl); - --clay-radius-pill: var(--radius-full); - --clay-duration-fast: 150ms; - --clay-duration-normal: 250ms; - --clay-duration-slow: 400ms; - --clay-easing: cubic-bezier(0.16, 1, 0.3, 1); - - /* Layout semantic tokens */ - --topbar-bg: var(--vscode-sidebar); - --topbar-shadow: none; - --topbar-border: 1px solid var(--glass-border); - --topbar-title-color: var(--vscode-text); - --topbar-title-weight: 600; - --sidebar-bg: var(--vscode-sidebar); - --sidebar-border: none; - --sidebar-nav-inactive: var(--vscode-muted); - --sidebar-nav-active-bg: var(--vscode-accent-alpha); - --sidebar-nav-active-bar: 3px solid var(--vscode-accent); - --agent-header-bg: var(--vscode-panel); - --agent-header-border: 1px solid var(--glass-border); - --agent-tab-active-bg: var(--vscode-accent-alpha); - --agent-tab-hover-bg: var(--vscode-accent-alpha); + color-scheme: dark; } -/* ─── Accessibility: High Contrast ─── */ +/* Light theme - Clean with subtle warmth */ +:root[data-theme="light"] { + --bg: #fafafa; + --bg-accent: #f5f5f5; + --bg-elevated: #ffffff; + --bg-hover: #f0f0f0; + --bg-muted: #f0f0f0; + --bg-content: #f5f5f5; -@media (prefers-contrast: more) { - :root { - --glass-shadow-sm: 0 0 0 2px var(--vscode-text); - --glass-shadow-md: 0 0 0 2px var(--vscode-text); - --glass-shadow-lg: 0 0 0 2px var(--vscode-text); - --glass-border: rgba(255, 255, 255, 0.3); - } + --card: #ffffff; + --card-foreground: #18181b; + --card-highlight: rgba(0, 0, 0, 0.03); + --popover: #ffffff; + --popover-foreground: #18181b; + + --panel: #fafafa; + --panel-strong: #f5f5f5; + --panel-hover: #ebebeb; + --chrome: rgba(250, 250, 250, 0.95); + --chrome-strong: rgba(250, 250, 250, 0.98); + + --text: #3f3f46; + --text-strong: #18181b; + --chat-text: #3f3f46; + --muted: #71717a; + --muted-strong: #52525b; + --muted-foreground: #71717a; + + --border: #e4e4e7; + --border-strong: #d4d4d8; + --border-hover: #a1a1aa; + --input: #e4e4e7; + + --accent: #dc2626; + --accent-hover: #ef4444; + --accent-muted: #dc2626; + --accent-subtle: rgba(220, 38, 38, 0.12); + --accent-foreground: #ffffff; + --accent-glow: rgba(220, 38, 38, 0.15); + --primary: #dc2626; + --primary-foreground: #ffffff; + + --secondary: #f4f4f5; + --secondary-foreground: #3f3f46; + --accent-2: #0d9488; + --accent-2-muted: rgba(13, 148, 136, 0.75); + --accent-2-subtle: rgba(13, 148, 136, 0.12); + + --ok: #16a34a; + --ok-muted: rgba(22, 163, 74, 0.75); + --ok-subtle: rgba(22, 163, 74, 0.1); + --destructive: #dc2626; + --destructive-foreground: #fafafa; + --warn: #d97706; + --warn-muted: rgba(217, 119, 6, 0.75); + --warn-subtle: rgba(217, 119, 6, 0.1); + --danger: #dc2626; + --danger-muted: rgba(220, 38, 38, 0.75); + --danger-subtle: rgba(220, 38, 38, 0.1); + --info: #2563eb; + + --focus: rgba(220, 38, 38, 0.2); + --focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 16px var(--accent-glow); + + --grid-line: rgba(0, 0, 0, 0.05); + + /* Light shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.04); + --shadow-glow: 0 0 24px var(--accent-glow); + + color-scheme: light; } -/* ════════════════════════════════════════════════════════ - Base Styles - ════════════════════════════════════════════════════════ */ +* { + box-sizing: border-box; +} html, body { @@ -572,8 +200,8 @@ body { body { margin: 0; - font: 400 15px/1.55 var(--font-body); - letter-spacing: -0.01em; + font: 400 14px/1.55 var(--font-body); + letter-spacing: -0.02em; background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; @@ -661,170 +289,7 @@ select { background: var(--border-strong); } -/* ════════════════════════════════════════════════════════ - Theme-Specific Decorative Effects - ════════════════════════════════════════════════════════ */ - -/* ─── Dark — Star field + ambient gradients ─── */ - -:root[data-theme="dark"] body { - background: - radial-gradient(ellipse 80% 50% at 50% -5%, rgba(0, 212, 170, 0.06) 0%, transparent 60%), - radial-gradient(ellipse 60% 40% at 60% 20%, rgba(202, 58, 41, 0.04) 0%, transparent 50%), - var(--bg); -} - -@keyframes star-twinkle { - 0% { - opacity: 0.35; - } - 100% { - opacity: 0.55; - } -} - -:root[data-theme="dark"] body::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; - opacity: 0.45; - animation: star-twinkle 5s ease-in-out infinite alternate; - box-shadow: - 120px 40px 0 0.4px rgba(0, 212, 170, 0.5), - 340px 90px 0 0.3px rgba(0, 212, 170, 0.3), - 580px 60px 0 0.5px rgba(0, 212, 170, 0.6), - 800px 130px 0 0.3px rgba(0, 212, 170, 0.4), - 1050px 50px 0 0.4px rgba(0, 212, 170, 0.3), - 90px 200px 0 0.5px rgba(0, 212, 170, 0.4), - 470px 220px 0 0.4px rgba(0, 212, 170, 0.5), - 900px 250px 0 0.5px rgba(0, 212, 170, 0.6), - 200px 420px 0 0.5px rgba(0, 212, 170, 0.5), - 640px 450px 0 0.4px rgba(0, 212, 170, 0.4), - 1060px 380px 0 0.5px rgba(0, 212, 170, 0.3), - 380px 580px 0 0.3px rgba(0, 212, 170, 0.4), - 780px 570px 0 0.3px rgba(0, 212, 170, 0.5), - 110px 680px 0 0.5px rgba(0, 212, 170, 0.4), - 520px 660px 0 0.4px rgba(0, 212, 170, 0.5); -} - -/* ─── openknot — Lavender stars ─── */ - -:root[data-theme="openknot"] body::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; - opacity: 0.35; - animation: star-twinkle 8s ease-in-out infinite alternate; - box-shadow: - 120px 40px 0 0.4px rgba(196, 181, 253, 0.5), - 340px 90px 0 0.3px rgba(196, 181, 253, 0.3), - 580px 60px 0 0.5px rgba(196, 181, 253, 0.6), - 800px 130px 0 0.3px rgba(196, 181, 253, 0.4), - 90px 200px 0 0.5px rgba(196, 181, 253, 0.4), - 470px 220px 0 0.4px rgba(196, 181, 253, 0.5), - 900px 250px 0 0.5px rgba(196, 181, 253, 0.6), - 200px 420px 0 0.5px rgba(196, 181, 253, 0.5), - 640px 450px 0 0.4px rgba(196, 181, 253, 0.4), - 380px 580px 0 0.3px rgba(196, 181, 253, 0.4), - 780px 570px 0 0.3px rgba(196, 181, 253, 0.5), - 520px 660px 0 0.4px rgba(196, 181, 253, 0.5); -} - -/* ─── fieldmanual — Industrial Dossier Overrides ─── */ - -:root[data-theme="fieldmanual"] .page-title, -:root[data-theme="fieldmanual"] .panel-title, -:root[data-theme="fieldmanual"] .agent-chat__welcome h2 { - font-family: var(--font-mono); - text-transform: uppercase; - letter-spacing: 0.04em; - font-weight: 700; -} - -:root[data-theme="fieldmanual"] .sidebar-brand__title { - font-family: var(--font-mono); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -:root[data-theme="fieldmanual"] .glass-dashboard-card, -:root[data-theme="fieldmanual"] .stat-card, -:root[data-theme="fieldmanual"] .agent-chat__starter { - border-style: dashed; -} - -:root[data-theme="fieldmanual"] .sidebar { - backdrop-filter: none; - -webkit-backdrop-filter: none; - background: var(--vscode-sidebar); -} - -:root[data-theme="fieldmanual"] .glass-dashboard-card { - backdrop-filter: none; - -webkit-backdrop-filter: none; - background: var(--vscode-panel); -} - -:root[data-theme="fieldmanual"] body::after { - display: none; -} - -/* ─── openai — Crimson atmosphere ─── */ - -:root[data-theme="openai"] body { - background: - radial-gradient(ellipse 80% 50% at 50% -5%, rgba(202, 58, 41, 0.12) 0%, transparent 60%), - radial-gradient(ellipse 60% 40% at 60% 20%, rgba(253, 142, 46, 0.04) 0%, transparent 50%), - var(--bg); -} - -:root[data-theme="openai"] body::after { - display: none; -} - -/* ─── clawdash — Chrome Metallic Overrides ─── */ - -:root[data-theme="clawdash"] body { - background: - radial-gradient(ellipse 80% 50% at 40% -10%, rgba(192, 200, 212, 0.06) 0%, transparent 60%), - radial-gradient(ellipse 60% 40% at 70% 30%, rgba(202, 58, 41, 0.04) 0%, transparent 50%), - var(--bg); -} - -:root[data-theme="clawdash"] body::after { - display: none; -} - -:root[data-theme="clawdash"] .nav-item--active { - border-image: linear-gradient(to bottom, var(--kn-silver-bright), var(--kn-claw)) 1; - border-image-slice: 1; -} - -/* ─── High Contrast Overrides (all themes) ─── */ - -@media (prefers-contrast: more) { - .topbar, - .sidebar, - .nav-item--active, - .stat-card, - .callout, - .pill, - pre, - input, - button { - box-shadow: 0 0 0 2px var(--text) !important; - border-width: 1.5px; - } -} - -/* ════════════════════════════════════════════════════════ - Animations - ════════════════════════════════════════════════════════ */ - +/* Animations - Polished with spring feel */ @keyframes rise { from { opacity: 0; @@ -896,15 +361,6 @@ select { } } -@keyframes chrome-shimmer { - 0% { - left: -100%; - } - 100% { - left: 100%; - } -} - /* Stagger animation delays for grouped elements */ .stagger-1 { animation-delay: 0ms; diff --git a/ui/src/styles/chat.css b/ui/src/styles/chat.css index d35b7316dde..07d3b644a63 100644 --- a/ui/src/styles/chat.css +++ b/ui/src/styles/chat.css @@ -3,4 +3,3 @@ @import "./chat/grouped.css"; @import "./chat/tool-cards.css"; @import "./chat/sidebar.css"; -@import "./chat/agent-chat.css"; diff --git a/ui/src/styles/chat/agent-chat.css b/ui/src/styles/chat/agent-chat.css deleted file mode 100644 index 13d4023a54b..00000000000 --- a/ui/src/styles/chat/agent-chat.css +++ /dev/null @@ -1,1287 +0,0 @@ -/* =========================================== - Agent Chat — ported from dashboard-lit - =========================================== */ - -.agent-chat { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - overflow: hidden; - position: relative; -} - -.agent-chat__thread { - flex: 1 1 0; - min-height: 0; - overflow-y: auto; - padding: 12px 18px; - display: flex; - flex-direction: column; - gap: 4px; -} - -.agent-chat__empty { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - color: var(--muted); - font-size: 0.92rem; -} - -.agent-chat__error { - color: color-mix(in srgb, var(--accent) 85%, #fff); - font-size: 0.85rem; - padding: 6px 10px; - margin-top: 4px; - background: color-mix(in srgb, var(--accent) 8%, transparent); - border-radius: var(--radius-sm); - border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); -} - -/* ─── Welcome / Empty State ─── */ - -.agent-chat__welcome { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 6px; - padding: 40px 24px 32px; - text-align: center; - position: relative; - overflow: hidden; -} - -.agent-chat__welcome-glow { - position: absolute; - top: 10%; - left: 50%; - transform: translateX(-50%); - width: 280px; - height: 180px; - border-radius: 50%; - background: radial-gradient(ellipse, var(--agent-color, var(--accent)) 0%, transparent 70%); - opacity: 0.06; - pointer-events: none; - filter: blur(40px); -} - -.agent-chat__welcome h2 { - font-size: 1.5rem; - font-weight: 700; - color: var(--text); - margin: 8px 0 0; - letter-spacing: -0.02em; -} - -.agent-chat__personality { - font-size: 0.88rem; - color: var(--muted); - max-width: 380px; - line-height: 1.55; - margin: 2px 0 0; -} - -.agent-chat__badges { - display: flex; - gap: 6px; - flex-wrap: wrap; - justify-content: center; - margin-top: 6px; -} - -.agent-chat__badge { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 4px 12px; - border-radius: 999px; - border: 1px solid var(--border); - background: transparent; - color: var(--muted); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.01em; -} - -.agent-chat__badge svg { - width: 14px; - height: 14px; -} - -/* ─── Starter Cards ─── */ - -.agent-chat__starters { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - margin-top: 16px; - width: 100%; - max-width: 420px; -} - -.agent-chat__starter { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 14px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--card); - color: var(--text); - font-size: 0.82rem; - font-weight: 500; - text-align: left; - cursor: pointer; - transition: - border-color var(--duration-fast) ease, - background var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - transform var(--duration-fast) var(--ease-spring); - line-height: 1.35; -} - -.agent-chat__starter:hover { - border-color: color-mix(in srgb, var(--agent-color, var(--accent)) 45%, transparent); - background: color-mix(in srgb, var(--agent-color, var(--accent)) 5%, transparent); - box-shadow: 0 2px 12px color-mix(in srgb, var(--agent-color, var(--accent)) 8%, transparent); - transform: translateY(-1px); -} - -.agent-chat__starter:active { - transform: translateY(0); - box-shadow: none; -} - -.agent-chat__starter:disabled { - opacity: 0.45; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -.agent-chat__starter-icon { - font-size: 1.15rem; - line-height: 1; - flex-shrink: 0; -} - -.agent-chat__starter-label { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.agent-chat__starter-arrow { - display: flex; - align-items: center; - color: var(--agent-color, var(--accent)); - opacity: 0; - transform: translateX(-3px); - transition: - opacity var(--duration-fast) ease, - transform var(--duration-fast) ease; - flex-shrink: 0; -} - -.agent-chat__starter-arrow svg { - width: 14px; - height: 14px; -} - -.agent-chat__starter:hover .agent-chat__starter-arrow { - opacity: 0.8; - transform: translateX(0); -} - -@media (max-width: 400px) { - .agent-chat__starters { - grid-template-columns: 1fr; - max-width: 280px; - } -} - -.agent-chat__hint { - font-size: 0.73rem; - color: var(--muted); - margin-top: 20px; - opacity: 0.7; -} - -.agent-chat__hint kbd { - display: inline-block; - padding: 1px 5px; - border: 1px solid var(--border); - border-radius: 4px; - background: var(--card); - font-size: 0.7rem; - font-family: inherit; -} - -/* ─── Avatar Circle ─── */ - -.agent-chat__avatar { - width: 56px; - height: 56px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.4rem; - font-weight: 700; - color: #fff; - background: var(--agent-color, var(--accent)); - flex-shrink: 0; -} - -.agent-chat__avatar--sm { - width: 24px; - height: 24px; - font-size: 0.65rem; -} - -/* ─── Chat Bubble ─── */ - -.chat-bubble { - padding: 10px 14px; - max-width: 100%; - word-wrap: break-word; - overflow-wrap: break-word; - position: relative; -} - -.chat-bubble--history { - opacity: 0.65; -} - -.chat-bubble--user { - background: color-mix(in srgb, var(--accent) 6%, var(--card)); - border-radius: var(--radius-lg); - border: 1px solid color-mix(in srgb, var(--accent) 14%, transparent); - margin-left: auto; - max-width: 85%; -} - -.chat-bubble--assistant { - padding: 10px 14px; -} - -.chat-bubble--tool { - padding: 4px 14px; -} - -.chat-bubble__header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.chat-bubble__role { - font-size: 0.78rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--ok); -} - -.chat-bubble--user .chat-bubble__role { - color: var(--accent); -} - -.chat-bubble__role--tool { - color: var(--warn); - display: inline-flex; - align-items: center; - gap: 4px; -} - -.chat-bubble__role--tool svg { - width: 14px; - height: 14px; -} - -.chat-bubble__model-tag { - font-size: 0.68rem; - font-weight: 600; - padding: 1px 6px; - border-radius: 999px; - background: color-mix(in srgb, var(--text) 8%, transparent); - color: var(--muted); -} - -.chat-bubble__ts { - font-size: 0.72rem; - color: var(--muted); -} - -.chat-bubble__body { - font-size: 0.92rem; - line-height: 1.45; - white-space: pre-wrap; - word-wrap: break-word; -} - -.chat-bubble__actions { - display: none; - gap: 4px; - margin-top: 4px; -} - -.chat-bubble:hover .chat-bubble__actions { - display: flex; -} - -.chat-bubble__action { - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border-radius: var(--radius-sm); - border: none; - background: transparent; - color: var(--muted); - cursor: pointer; - transition: all var(--duration-fast) ease; - padding: 0; -} - -.chat-bubble__action svg { - width: 14px; - height: 14px; -} - -.chat-bubble__action:hover { - color: var(--text); - background: var(--bg-hover); -} - -/* ─── Chat Divider ─── */ - -.agent-chat__divider { - display: flex; - align-items: center; - gap: 12px; - margin: 10px 0; - font-size: 0.72rem; - color: var(--accent); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.agent-chat__divider::before, -.agent-chat__divider::after { - content: ""; - flex: 1; - height: 1px; - background: color-mix(in srgb, var(--accent) 30%, transparent); -} - -/* ─── Streaming Indicator ─── */ - -.agent-chat__streaming { - padding: 10px 14px; - border-left: 2px solid var(--accent); - animation: chat-pulse 1.5s ease-in-out infinite; -} - -.agent-chat__streaming-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 6px; -} - -.agent-chat__streaming-name { - font-size: 0.82rem; - font-weight: 600; - color: var(--text); -} - -.agent-chat__streaming-dots { - display: inline-flex; - gap: 3px; - align-items: center; -} - -.agent-chat__streaming-dots span { - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--accent); - animation: chat-pulse 1.2s ease-in-out infinite; -} - -.agent-chat__streaming-dots span:nth-child(2) { - animation-delay: 0.2s; -} - -.agent-chat__streaming-dots span:nth-child(3) { - animation-delay: 0.4s; -} - -.agent-chat__streaming-label { - font-size: 0.75rem; - color: var(--muted); - font-style: italic; -} - -.agent-chat__streaming-timer { - font-size: 0.72rem; - color: var(--muted); - font-variant-numeric: tabular-nums; -} - -.agent-chat__streaming-content { - font-size: 0.92rem; - line-height: 1.45; -} - -.agent-chat__cursor { - display: inline-block; - width: 2px; - height: 1em; - background: var(--accent); - margin-left: 1px; - vertical-align: text-bottom; - animation: cursor-blink 0.8s step-end infinite; -} - -@keyframes cursor-blink { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0; - } -} - -@keyframes chat-pulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } -} - -/* ─── Input Bar (Cursor-style unified container) ─── */ - -.agent-chat__input { - position: relative; - display: flex; - flex-direction: column; - margin: 0 18px 14px; - padding: 0; - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - flex-shrink: 0; - overflow: hidden; - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; -} - -.agent-chat__input:focus-within { - border-color: color-mix(in srgb, var(--accent) 50%, transparent); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 10%, transparent); -} - -@supports (backdrop-filter: blur(1px)) { - .agent-chat__input { - backdrop-filter: blur(16px) saturate(1.8); - -webkit-backdrop-filter: blur(16px) saturate(1.8); - } -} - -/* Textarea — full width, borderless inside the container */ - -.agent-chat__input > textarea { - width: 100%; - min-height: 40px; - max-height: 150px; - resize: none; - padding: 12px 14px 8px; - border: none; - background: transparent; - color: var(--text); - font-size: 0.92rem; - font-family: inherit; - line-height: 1.4; - outline: none; - box-sizing: border-box; -} - -.agent-chat__input > textarea::placeholder { - color: var(--muted); -} - -/* ─── Toolbar (below textarea) ─── */ - -.agent-chat__toolbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 10px; - border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); -} - -.agent-chat__toolbar-left, -.agent-chat__toolbar-right { - display: flex; - align-items: center; - gap: 4px; -} - -/* ─── Toolbar buttons (ghost style) ─── */ - -.agent-chat__input-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - border-radius: var(--radius-sm); - border: none; - background: transparent; - color: var(--muted); - cursor: pointer; - flex-shrink: 0; - transition: all var(--duration-fast) ease; - padding: 0; -} - -.agent-chat__input-btn svg { - width: 16px; - height: 16px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.agent-chat__input-btn:hover:not(:disabled) { - color: var(--text); - background: var(--bg-hover); -} - -.agent-chat__input-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.agent-chat__input-btn--active { - color: var(--accent); - background: color-mix(in srgb, var(--accent) 12%, transparent); -} - -.agent-chat__toolbar .btn-ghost { - display: inline-flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - border-radius: var(--radius-sm); - border: none; - background: transparent; - color: var(--muted); - cursor: pointer; - padding: 0; - transition: all var(--duration-fast) ease; -} - -.agent-chat__toolbar .btn-ghost svg { - width: 16px; - height: 16px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.agent-chat__toolbar .btn-ghost:hover:not(:disabled) { - color: var(--text); - background: var(--bg-hover); -} - -.agent-chat__toolbar .btn-ghost:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.agent-chat__input-divider { - width: 1px; - height: 16px; - background: var(--border); - margin: 0 4px; -} - -.agent-chat__token-count { - font-size: 0.7rem; - color: var(--muted); - white-space: nowrap; - flex-shrink: 0; - align-self: center; -} - -/* Send / Stop button */ - -.chat-send-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: var(--radius-md); - border: none; - background: var(--accent); - color: var(--accent-foreground); - cursor: pointer; - flex-shrink: 0; - transition: all var(--duration-fast) ease; - padding: 0; -} - -.chat-send-btn svg { - width: 16px; - height: 16px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.chat-send-btn:hover:not(:disabled) { - background: var(--accent-hover); - box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 24%, transparent); -} - -.chat-send-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.chat-send-btn--stop { - background: var(--danger); -} - -.chat-send-btn--stop:hover:not(:disabled) { - background: color-mix(in srgb, var(--danger) 85%, #fff); -} - -/* ─── Search Bar ─── */ - -.agent-chat__search-bar { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 14px; - border-bottom: 1px solid var(--border); - background: var(--card); -} - -.agent-chat__search-bar svg { - width: 16px; - height: 16px; - color: var(--muted); - flex-shrink: 0; -} - -.agent-chat__search-bar input { - flex: 1; - border: none; - background: transparent; - color: var(--text); - font-size: 0.88rem; - outline: none; -} - -.agent-chat__search-bar input::placeholder { - color: var(--muted); -} - -/* ─── Pinned Messages ─── */ - -.agent-chat__pinned { - border-bottom: 1px solid var(--border); - padding: 6px 14px; -} - -.agent-chat__pinned-toggle { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: var(--radius-sm); - border: none; - background: transparent; - color: var(--accent); - font-size: 0.78rem; - font-weight: 600; - cursor: pointer; - transition: background var(--duration-fast) ease; -} - -.agent-chat__pinned-toggle svg { - width: 14px; - height: 14px; -} - -.agent-chat__pinned-toggle:hover { - background: var(--bg-hover); -} - -.agent-chat__pinned-list { - display: flex; - flex-direction: column; - gap: 4px; - margin-top: 4px; - padding-left: 8px; -} - -.agent-chat__pinned-item { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 8px; - border-radius: var(--radius-sm); - font-size: 0.82rem; -} - -.agent-chat__pinned-role { - font-weight: 600; - font-size: 0.72rem; - text-transform: uppercase; - color: var(--muted); - flex-shrink: 0; -} - -.agent-chat__pinned-text { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--text); -} - -/* ─── Scroll Pill ─── */ - -.agent-chat__scroll-pill { - position: absolute; - bottom: 100px; - left: 50%; - transform: translateX(-50%); - display: inline-flex; - align-items: center; - gap: 5px; - padding: 6px 14px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--card); - color: var(--accent); - font-size: 0.78rem; - font-weight: 600; - cursor: pointer; - box-shadow: var(--shadow-md); - z-index: 20; - transition: all var(--duration-fast) ease; -} - -.agent-chat__scroll-pill svg { - width: 14px; - height: 14px; -} - -.agent-chat__scroll-pill:hover { - background: color-mix(in srgb, var(--accent) 10%, var(--card)); -} - -/* ─── Slash Command Menu ─── */ - -.slash-menu { - position: absolute; - bottom: 100%; - left: 0; - right: 0; - max-height: 320px; - overflow-y: auto; - background: var(--popover); - border: 1px solid var(--border-strong); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - z-index: 30; - margin-bottom: 4px; - padding: 6px; - scrollbar-width: thin; -} - -.slash-menu-group + .slash-menu-group { - margin-top: 4px; - padding-top: 4px; - border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); -} - -.slash-menu-group__label { - padding: 4px 10px 2px; - font-size: 0.68rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--accent); - opacity: 0.7; -} - -.slash-menu-item { - display: flex; - align-items: center; - gap: 8px; - padding: 7px 10px; - border-radius: var(--radius-sm); - cursor: pointer; - transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease; -} - -.slash-menu-item:hover, -.slash-menu-item--active { - background: color-mix(in srgb, var(--accent) 10%, var(--bg-hover)); -} - -.slash-menu-icon { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - flex-shrink: 0; - color: var(--accent); - opacity: 0.7; -} - -.slash-menu-icon svg { - width: 14px; - height: 14px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.slash-menu-item--active .slash-menu-icon, -.slash-menu-item:hover .slash-menu-icon { - opacity: 1; -} - -.slash-menu-name { - font-size: 0.82rem; - font-weight: 600; - font-family: var(--mono); - color: var(--accent); - white-space: nowrap; -} - -.slash-menu-args { - font-size: 0.75rem; - color: var(--muted); - font-family: var(--mono); - opacity: 0.65; -} - -.slash-menu-desc { - font-size: 0.75rem; - color: var(--muted); - flex: 1; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.slash-menu-item--active .slash-menu-name { - color: var(--accent-hover); -} - -.slash-menu-item--active .slash-menu-desc { - color: var(--text); -} - -/* ─── Attachment Previews ─── */ - -.chat-attachments-preview { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px; -} - -.chat-attachment-thumb { - position: relative; - width: 60px; - height: 60px; - border-radius: var(--radius-sm); - overflow: hidden; - border: 1px solid var(--border); -} - -.chat-attachment-thumb img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.chat-attachment-remove { - position: absolute; - top: 2px; - right: 2px; - width: 18px; - height: 18px; - border-radius: 50%; - border: none; - background: rgba(0, 0, 0, 0.6); - color: #fff; - font-size: 12px; - line-height: 1; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} - -.chat-attachment-file { - display: flex; - align-items: center; - gap: 4px; - font-size: 0.72rem; - color: var(--muted); - padding: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* ─── Reasoning Block ─── */ - -.reasoning-block { - margin: 4px 0; -} - -.reasoning-block__toggle { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - border: 1px solid var(--border); - border-radius: 999px; - background: var(--bg-hover); - color: var(--muted); - font-size: 0.75rem; - font-weight: 600; - cursor: pointer; - transition: all var(--duration-fast) ease; -} - -.reasoning-block__toggle:hover { - color: var(--text); - border-color: var(--border-strong); -} - -.reasoning-block__content { - display: none; - margin-top: 6px; - padding: 8px 12px; - font-size: 0.82rem; - line-height: 1.5; - color: var(--muted); - font-style: italic; - white-space: pre-wrap; - word-wrap: break-word; - border-left: 2px solid var(--border); -} - -.reasoning-block--open .reasoning-block__content { - display: block; -} - -.reasoning-block--streaming .reasoning-block__toggle { - animation: chat-pulse 1.5s ease-in-out infinite; -} - -/* ─── Tool Block ─── */ - -.tool-block { - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg); - overflow: hidden; - margin: 4px 0; -} - -.tool-block__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 8px 12px; - cursor: pointer; - font-size: 0.82rem; - font-weight: 600; - color: var(--text); - transition: background var(--duration-fast) ease; -} - -.tool-block__header:hover { - background: var(--bg-hover); -} - -.tool-block__name { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.tool-block__name svg { - width: 14px; - height: 14px; -} - -.tool-block__body { - display: none; - padding: 0 12px 10px; -} - -.tool-block--open .tool-block__body { - display: block; -} - -.tool-block__output { - margin: 0; - font-family: var(--mono); - font-size: 0.78rem; - line-height: 1.5; - color: var(--muted); - white-space: pre-wrap; - word-wrap: break-word; - max-height: 300px; - overflow: auto; - padding: 8px; - border-radius: var(--radius-sm); - background: var(--bg-accent); - border: 1px solid var(--border); -} - -.tool-block__chevron { - transition: transform var(--duration-fast) ease; -} - -.tool-block__chevron svg { - width: 14px; - height: 14px; -} - -.tool-block--open .tool-block__chevron { - transform: rotate(180deg); -} - -/* ─── File Input (hidden) ─── */ - -.agent-chat__file-input { - display: none; -} - -/* ─── Danger ghost button ─── */ - -.btn-ghost--danger:hover { - color: var(--danger) !important; -} - -.btn-ghost--sm { - padding: 4px; -} - -.btn-ghost--sm svg { - width: 14px; - height: 14px; -} - -/* ─── Agent Bar ─── */ - -.chat-agent-bar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 16px; - border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); - flex-shrink: 0; - gap: 8px; -} - -.chat-agent-bar__left { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; -} - -.chat-agent-bar__right { - display: flex; - align-items: center; - gap: 4px; - flex-shrink: 0; -} - -.chat-agent-bar__name { - font-size: 13px; - font-weight: 600; - color: var(--text); -} - -.chat-agent-select { - background: color-mix(in srgb, var(--secondary) 70%, transparent); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text); - font-size: 13px; - font-weight: 500; - padding: 4px 24px 4px 8px; - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237d8590' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 6px center; - transition: - border-color 150ms ease, - background 150ms ease; -} - -.chat-agent-select:hover { - border-color: var(--border-strong); - background: color-mix(in srgb, var(--secondary) 90%, transparent); -} - -.chat-agent-select:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -/* ─── Sessions Panel ─── */ - -.chat-sessions-panel { - position: relative; -} - -.chat-sessions-summary { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 3px 8px; - border-radius: var(--radius-md); - font-size: 12px; - font-weight: 500; - color: var(--muted); - cursor: pointer; - user-select: none; - list-style: none; - transition: - color 150ms ease, - background 150ms ease; -} - -.chat-sessions-summary::-webkit-details-marker { - display: none; -} - -.chat-sessions-summary::before { - content: "▸"; - font-size: 9px; - transition: transform 150ms ease; -} - -.chat-sessions-panel[open] > .chat-sessions-summary::before { - transform: rotate(90deg); -} - -.chat-sessions-summary:hover { - color: var(--text); - background: color-mix(in srgb, var(--bg-hover) 60%, transparent); -} - -.chat-sessions-summary svg { - width: 13px; - height: 13px; -} - -.chat-sessions-list { - position: absolute; - top: 100%; - left: 0; - z-index: 50; - min-width: 240px; - max-width: 360px; - max-height: 280px; - overflow-y: auto; - margin-top: 4px; - padding: 4px; - background: var(--popover); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - display: flex; - flex-direction: column; - gap: 2px; -} - -.chat-session-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 6px 10px; - border: none; - border-radius: var(--radius-sm); - background: none; - color: var(--text); - font-size: 12px; - cursor: pointer; - text-align: left; - width: 100%; - transition: background 120ms ease; -} - -.chat-session-item:hover { - background: var(--bg-hover); -} - -.chat-session-item--active { - background: var(--accent-subtle); - color: var(--accent); - font-weight: 500; -} - -.chat-session-item__name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; -} - -.chat-session-item__meta { - font-size: 11px; - flex-shrink: 0; - white-space: nowrap; -} diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 46cd18f4e24..c43743267a9 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -83,15 +83,14 @@ /* Avatar Styles */ .chat-avatar { - width: 38px; - height: 38px; - border-radius: var(--radius-md); - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - background: color-mix(in srgb, var(--panel-strong) 95%, transparent); + width: 40px; + height: 40px; + border-radius: 8px; + background: var(--panel-strong); display: grid; place-items: center; font-weight: 600; - font-size: 13px; + font-size: 14px; flex-shrink: 0; align-self: flex-end; /* Align with last message in group */ margin-bottom: 4px; /* Optical alignment */ @@ -128,15 +127,14 @@ img.chat-avatar { .chat-bubble { position: relative; display: inline-block; - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - background: color-mix(in srgb, var(--card) 97%, transparent); + border: 1px solid transparent; + background: var(--card); border-radius: var(--radius-lg); padding: 10px 14px; - box-shadow: inset 0 1px 0 var(--card-highlight); + box-shadow: none; transition: background 150ms ease-out, - border-color 150ms ease-out, - box-shadow 150ms ease-out; + border-color 150ms ease-out; max-width: 100%; word-wrap: break-word; } @@ -149,8 +147,8 @@ img.chat-avatar { position: absolute; top: 6px; right: 8px; - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - background: color-mix(in srgb, var(--bg) 94%, transparent); + border: 1px solid var(--border); + background: var(--bg); color: var(--muted); border-radius: var(--radius-md); padding: 4px 6px; @@ -161,8 +159,7 @@ img.chat-avatar { pointer-events: none; transition: opacity 120ms ease-out, - background 120ms ease-out, - border-color 120ms ease-out; + background 120ms ease-out; } .chat-copy-btn__icon { @@ -209,7 +206,6 @@ img.chat-avatar { .chat-copy-btn:hover { background: var(--bg-hover); - border-color: var(--border-strong); } .chat-copy-btn[data-copying="1"] { @@ -247,20 +243,29 @@ img.chat-avatar { } } +/* Light mode: restore borders */ +:root[data-theme="light"] .chat-bubble { + border-color: var(--border); + box-shadow: inset 0 1px 0 var(--card-highlight); +} + .chat-bubble:hover { - background: color-mix(in srgb, var(--card) 82%, var(--bg-hover)); - border-color: var(--border-strong); - box-shadow: var(--shadow-sm); + background: var(--bg-hover); } /* User bubbles have different styling */ .chat-group.user .chat-bubble { - background: color-mix(in srgb, var(--accent-subtle) 85%, var(--secondary)); - border-color: color-mix(in srgb, var(--accent) 30%, transparent); + background: var(--accent-subtle); + border-color: transparent; +} + +:root[data-theme="light"] .chat-group.user .chat-bubble { + border-color: rgba(234, 88, 12, 0.2); + background: rgba(251, 146, 60, 0.12); } .chat-group.user .chat-bubble:hover { - background: var(--danger-subtle); + background: rgba(255, 77, 77, 0.15); } /* Streaming animation */ @@ -293,59 +298,3 @@ img.chat-avatar { transform: translateY(0); } } - -/* Delete button (appears on hover in group footer) */ - -.chat-group-delete { - all: unset; - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - border-radius: var(--radius-sm); - color: var(--muted); - cursor: pointer; - opacity: 0; - pointer-events: none; - transition: - opacity 120ms ease-out, - color 120ms ease-out, - background 120ms ease-out; - margin-left: auto; -} - -.chat-group-delete svg { - width: 12px; - height: 12px; - stroke: currentColor; - fill: none; - stroke-width: 2px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.chat-group:hover .chat-group-delete { - opacity: 0.5; - pointer-events: auto; -} - -.chat-group-delete:hover { - opacity: 1 !important; - color: var(--danger); - background: var(--danger-subtle); -} - -.chat-group-delete:focus-visible { - opacity: 1; - pointer-events: auto; - outline: 2px solid var(--accent); - outline-offset: 1px; -} - -@media (hover: none) { - .chat-group-delete { - opacity: 0.5; - pointer-events: auto; - } -} diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 6c4123de8d3..4a5c4cdfa46 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -52,15 +52,11 @@ flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */ overflow-y: auto; overflow-x: hidden; - padding: 14px 8px; + padding: 12px 4px; margin: 0 -4px; min-height: 0; /* Allow shrinking for flex scroll behavior */ - border-radius: var(--radius-lg); - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--panel) 72%, transparent), - transparent - ); + border-radius: 12px; + background: transparent; } /* Focus mode exit button */ @@ -115,22 +111,20 @@ font-size: 13px; font-family: var(--font-body); color: var(--text); - background: color-mix(in srgb, var(--panel-strong) 92%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 86%, transparent); + background: var(--panel-strong); + border: 1px solid var(--border); border-radius: 999px; cursor: pointer; white-space: nowrap; z-index: 10; transition: background 150ms ease-out, - border-color 150ms ease-out, - box-shadow 150ms ease-out; + border-color 150ms ease-out; } .chat-new-messages:hover { background: var(--panel); - border-color: color-mix(in srgb, var(--accent) 36%, transparent); - box-shadow: var(--shadow-sm); + border-color: var(--accent); } .chat-new-messages svg { @@ -153,9 +147,8 @@ flex-direction: column; gap: 12px; margin-top: auto; /* Push to bottom of flex container */ - padding: 14px 6px 6px; - background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--bg) 94%, black) 22%); - backdrop-filter: blur(4px); + padding: 12px 4px 4px; + background: linear-gradient(to bottom, transparent, var(--bg) 20%); z-index: 10; } @@ -225,6 +218,21 @@ stroke-width: 2px; } +/* Light theme attachment overrides */ +:root[data-theme="light"] .chat-attachments { + background: #f8fafc; + border-color: rgba(16, 24, 40, 0.1); +} + +:root[data-theme="light"] .chat-attachment { + border-color: rgba(16, 24, 40, 0.15); + background: #fff; +} + +:root[data-theme="light"] .chat-attachment__remove { + background: rgba(0, 0, 0, 0.6); +} + /* Message images (sent images displayed in chat) */ .chat-message-images { display: flex; @@ -259,6 +267,10 @@ flex: 1; } +:root[data-theme="light"] .chat-compose { + background: linear-gradient(to bottom, transparent, var(--bg-content) 20%); +} + .chat-compose__field { flex: 1 1 auto; min-width: 0; @@ -278,16 +290,13 @@ min-height: 40px; max-height: 150px; padding: 9px 12px; - border-radius: var(--radius-md); + border-radius: 8px; overflow-y: auto; resize: none; white-space: pre-wrap; font-family: var(--font-body); font-size: 14px; line-height: 1.45; - border: 1px solid color-mix(in srgb, var(--input) 92%, transparent); - background: color-mix(in srgb, var(--card) 98%, transparent); - box-shadow: inset 0 1px 0 var(--card-highlight); } .chat-compose__field textarea:disabled { @@ -342,22 +351,25 @@ display: inline-flex; align-items: center; justify-content: center; - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - background: color-mix(in srgb, var(--secondary) 85%, transparent); - border-radius: var(--radius-md); + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.06); } /* Controls separator */ .chat-controls__separator { - color: var(--border); + color: rgba(255, 255, 255, 0.4); font-size: 18px; margin: 0 8px; font-weight: 300; } +:root[data-theme="light"] .chat-controls__separator { + color: rgba(16, 24, 40, 0.3); +} + .btn--icon:hover { - background: var(--bg-hover); - border-color: var(--border-strong); + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.2); } /* Light theme icon button overrides */ @@ -374,6 +386,27 @@ color: var(--text); } +/* Light theme icon button overrides */ +:root[data-theme="light"] .btn--icon { + background: #ffffff; + border-color: var(--border); + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + color: var(--muted); +} + +:root[data-theme="light"] .btn--icon:hover { + background: #ffffff; + border-color: var(--border-strong); + color: var(--text); +} + +:root[data-theme="light"] .chat-controls .btn--icon.active { + border-color: var(--accent); + background: var(--accent-subtle); + color: var(--accent); + box-shadow: 0 0 0 1px var(--accent-subtle); +} + .btn--icon svg { display: block; width: 18px; @@ -399,9 +432,15 @@ gap: 4px; font-size: 12px; padding: 4px 10px; - background: color-mix(in srgb, var(--secondary) 90%, transparent); - border-radius: var(--radius-sm); - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + background: rgba(255, 255, 255, 0.04); + border-radius: 6px; + border: 1px solid var(--border); +} + +/* Light theme thinking indicator override */ +:root[data-theme="light"] .chat-controls__thinking { + background: rgba(255, 255, 255, 0.9); + border-color: rgba(16, 24, 40, 0.15); } @media (max-width: 640px) { diff --git a/ui/src/styles/chat/sidebar.css b/ui/src/styles/chat/sidebar.css index bc2949309d5..934e285d95b 100644 --- a/ui/src/styles/chat/sidebar.css +++ b/ui/src/styles/chat/sidebar.css @@ -19,12 +19,11 @@ .chat-sidebar { flex: 1; min-width: 300px; - border-left: 1px solid color-mix(in srgb, var(--border) 90%, transparent); + border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; animation: slide-in 200ms ease-out; - background: color-mix(in srgb, var(--panel) 94%, transparent); } @keyframes slide-in { @@ -51,13 +50,12 @@ justify-content: space-between; align-items: center; padding: 12px 16px; - border-bottom: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + border-bottom: 1px solid var(--border); flex-shrink: 0; position: sticky; top: 0; z-index: 10; - background: color-mix(in srgb, var(--panel) 95%, transparent); - backdrop-filter: blur(6px); + background: var(--panel); } /* Smaller close button for sidebar */ @@ -81,13 +79,12 @@ .sidebar-markdown { font-size: 14px; - line-height: 1.6; + line-height: 1.5; } .sidebar-markdown pre { - background: color-mix(in srgb, var(--secondary) 90%, transparent); - border-radius: var(--radius-md); - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + background: rgba(0, 0, 0, 0.12); + border-radius: 4px; padding: 12px; overflow-x: auto; } diff --git a/ui/src/styles/chat/text.css b/ui/src/styles/chat/text.css index ead2a69058e..d6eea9866b2 100644 --- a/ui/src/styles/chat/text.css +++ b/ui/src/styles/chat/text.css @@ -5,12 +5,17 @@ .chat-thinking { margin-bottom: 10px; padding: 10px 12px; - border-radius: var(--radius-md); - border: 1px dashed color-mix(in srgb, var(--border) 84%, transparent); - background: color-mix(in srgb, var(--secondary) 75%, transparent); + border-radius: 10px; + border: 1px dashed rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.04); color: var(--muted); font-size: 12px; - line-height: 1.45; + line-height: 1.4; +} + +:root[data-theme="light"] .chat-thinking { + border-color: rgba(16, 24, 40, 0.25); + background: rgba(16, 24, 40, 0.04); } .chat-text { @@ -52,16 +57,14 @@ } .chat-text :where(:not(pre) > code) { - background: color-mix(in srgb, var(--secondary) 82%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); - padding: 0.15em 0.42em; - border-radius: 5px; + background: rgba(0, 0, 0, 0.15); + padding: 0.15em 0.4em; + border-radius: 4px; } .chat-text :where(pre) { - background: color-mix(in srgb, var(--secondary) 82%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); - border-radius: var(--radius-md); + background: rgba(0, 0, 0, 0.15); + border-radius: 6px; padding: 10px 12px; overflow-x: auto; } @@ -71,50 +74,12 @@ padding: 0; } -/* Collapsed JSON code blocks */ - -.chat-text :where(details.json-collapse) { - background: color-mix(in srgb, var(--secondary) 82%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); - border-radius: var(--radius-md); -} - -.chat-text :where(details.json-collapse > summary) { - padding: 8px 12px; - cursor: pointer; - font-size: 12px; - color: var(--muted); - font-family: var(--mono); - user-select: none; - list-style: none; -} - -.chat-text :where(details.json-collapse > summary::-webkit-details-marker) { - display: none; -} - -.chat-text :where(details.json-collapse > summary::before) { - content: "▸ "; -} - -.chat-text :where(details.json-collapse[open] > summary::before) { - content: "▾ "; -} - -.chat-text :where(details.json-collapse > pre) { - background: none; - border: none; - border-top: 1px solid color-mix(in srgb, var(--border) 84%, transparent); - border-radius: 0; - margin: 0; -} - .chat-text :where(blockquote) { - border-left: 3px solid color-mix(in srgb, var(--border-strong) 88%, transparent); + border-left: 3px solid var(--border-strong); padding-left: 12px; margin-left: 0; color: var(--muted); - background: color-mix(in srgb, var(--secondary) 78%, transparent); + background: rgba(255, 255, 255, 0.02); padding: 8px 12px; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; } @@ -122,12 +87,34 @@ .chat-text :where(blockquote blockquote) { margin-top: 8px; border-left-color: var(--border-hover); - background: color-mix(in srgb, var(--secondary) 55%, transparent); + background: rgba(255, 255, 255, 0.03); } .chat-text :where(blockquote blockquote blockquote) { border-left-color: var(--muted-strong); - background: color-mix(in srgb, var(--secondary) 60%, transparent); + background: rgba(255, 255, 255, 0.04); +} + +:root[data-theme="light"] .chat-text :where(blockquote) { + background: rgba(0, 0, 0, 0.03); +} + +:root[data-theme="light"] .chat-text :where(blockquote blockquote) { + background: rgba(0, 0, 0, 0.05); +} + +:root[data-theme="light"] .chat-text :where(blockquote blockquote blockquote) { + background: rgba(0, 0, 0, 0.04); +} + +:root[data-theme="light"] .chat-text :where(:not(pre) > code) { + background: rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.1); +} + +:root[data-theme="light"] .chat-text :where(pre) { + background: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.1); } .chat-text :where(hr) { diff --git a/ui/src/styles/chat/tool-cards.css b/ui/src/styles/chat/tool-cards.css index c1e478aa9fc..6384db115f0 100644 --- a/ui/src/styles/chat/tool-cards.css +++ b/ui/src/styles/chat/tool-cards.css @@ -1,15 +1,14 @@ /* Tool Card Styles */ .chat-tool-card { - border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); - border-radius: var(--radius-md); + border: 1px solid var(--border); + border-radius: 8px; padding: 12px; margin-top: 8px; - background: color-mix(in srgb, var(--card) 97%, transparent); + background: var(--card); box-shadow: inset 0 1px 0 var(--card-highlight); transition: border-color 150ms ease-out, - background 150ms ease-out, - box-shadow 150ms ease-out; + background 150ms ease-out; /* Fixed max-height to ensure cards don't expand too much */ max-height: 120px; overflow: hidden; @@ -17,8 +16,7 @@ .chat-tool-card:hover { border-color: var(--border-strong); - background: color-mix(in srgb, var(--card) 82%, var(--bg-hover)); - box-shadow: var(--shadow-sm); + background: var(--bg-hover); } /* First tool card in a group - no top margin */ @@ -130,13 +128,13 @@ color: var(--muted); margin-top: 8px; padding: 8px 10px; - background: color-mix(in srgb, var(--secondary) 92%, transparent); + background: var(--secondary); border-radius: var(--radius-md); white-space: pre-wrap; overflow: hidden; max-height: 44px; line-height: 1.4; - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + border: 1px solid var(--border); } .chat-tool-card--clickable:hover .chat-tool-card__preview { @@ -150,18 +148,16 @@ color: var(--text); margin-top: 6px; padding: 6px 8px; - background: color-mix(in srgb, var(--secondary) 92%, transparent); + background: var(--secondary); border-radius: var(--radius-sm); - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); white-space: pre-wrap; word-break: break-word; } /* Reading Indicator */ .chat-reading-indicator { - background: color-mix(in srgb, var(--secondary) 70%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - border-radius: var(--radius-md); + background: transparent; + border: 1px solid var(--border); padding: 12px; display: inline-flex; } @@ -204,176 +200,3 @@ transform: scale(1); } } - -/* =========================================== - Collapsible Tool Cards - =========================================== */ - -.chat-tools-collapse { - margin-top: 8px; - border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); - border-radius: var(--radius-md); - background: color-mix(in srgb, var(--card) 94%, transparent); - overflow: hidden; -} - -.chat-tools-summary { - display: flex; - align-items: center; - gap: 6px; - padding: 8px 12px; - cursor: pointer; - font-size: 12px; - font-weight: 500; - color: var(--muted); - user-select: none; - list-style: none; - transition: - color 150ms ease, - background 150ms ease; -} - -.chat-tools-summary::-webkit-details-marker { - display: none; -} - -.chat-tools-summary::before { - content: "▸"; - font-size: 10px; - flex-shrink: 0; - transition: transform 150ms ease; -} - -.chat-tools-collapse[open] > .chat-tools-summary::before { - transform: rotate(90deg); -} - -.chat-tools-summary:hover { - color: var(--text); - background: color-mix(in srgb, var(--bg-hover) 50%, transparent); -} - -.chat-tools-summary__icon { - display: inline-flex; - align-items: center; - width: 14px; - height: 14px; - color: var(--accent); - opacity: 0.7; - flex-shrink: 0; -} - -.chat-tools-summary__icon svg { - width: 14px; - height: 14px; -} - -.chat-tools-summary__count { - font-weight: 600; - color: var(--text); -} - -.chat-tools-summary__names { - color: var(--muted); - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.chat-tools-collapse__body { - padding: 4px 12px 12px; - border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); -} - -.chat-tools-collapse__body .chat-tool-card:first-child { - margin-top: 8px; -} - -/* =========================================== - Collapsible JSON Block - =========================================== */ - -.chat-json-collapse { - margin-top: 4px; - border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); - border-radius: var(--radius-md); - background: color-mix(in srgb, var(--secondary) 60%, transparent); - overflow: hidden; -} - -.chat-json-summary { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 10px; - cursor: pointer; - font-size: 12px; - color: var(--muted); - user-select: none; - list-style: none; - transition: - color 150ms ease, - background 150ms ease; -} - -.chat-json-summary::-webkit-details-marker { - display: none; -} - -.chat-json-summary::before { - content: "▸"; - font-size: 10px; - flex-shrink: 0; - transition: transform 150ms ease; -} - -.chat-json-collapse[open] > .chat-json-summary::before { - transform: rotate(90deg); -} - -.chat-json-summary:hover { - color: var(--text); - background: color-mix(in srgb, var(--bg-hover) 50%, transparent); -} - -.chat-json-badge { - display: inline-flex; - align-items: center; - padding: 1px 5px; - border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--accent) 15%, transparent); - color: var(--accent); - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - line-height: 1.4; - flex-shrink: 0; -} - -.chat-json-label { - font-family: var(--mono); - font-size: 11px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.chat-json-content { - margin: 0; - padding: 10px 12px; - border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); - font-family: var(--mono); - font-size: 12px; - line-height: 1.5; - color: var(--text); - overflow-x: auto; - max-height: 400px; - overflow-y: auto; -} - -.chat-json-content code { - font-family: inherit; - font-size: inherit; -} diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index e77a471f64b..09b89d9c270 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1,79 +1,5 @@ @import "./chat.css"; -/* =========================================== - Login Gate - =========================================== */ - -.login-gate { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - min-height: 100dvh; - background: var(--bg); - padding: 24px; -} - -.login-gate__theme { - position: fixed; - top: 16px; - right: 16px; - z-index: 10; -} - -.login-gate__card { - width: min(520px, 100%); - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 32px; - animation: scale-in 0.25s var(--ease-out); -} - -.login-gate__header { - text-align: center; - margin-bottom: 24px; -} - -.login-gate__logo { - width: 48px; - height: 48px; - margin-bottom: 12px; -} - -.login-gate__title { - font-size: 22px; - font-weight: 700; - letter-spacing: -0.02em; -} - -.login-gate__sub { - color: var(--muted); - font-size: 14px; - margin-top: 4px; -} - -.login-gate__form { - display: flex; - flex-direction: column; - gap: 12px; -} - -.login-gate__connect { - margin-top: 4px; - width: 100%; - justify-content: center; - padding: 10px 16px; - font-size: 15px; - font-weight: 600; -} - -.login-gate__help { - margin-top: 20px; - padding-top: 16px; - border-top: 1px solid var(--border); -} - /* =========================================== Update Banner =========================================== */ @@ -100,7 +26,7 @@ } .update-banner__btn:hover:not(:disabled) { - background: var(--danger-subtle); + background: rgba(239, 68, 68, 0.15); } /* =========================================== @@ -130,7 +56,7 @@ } .card-title { - font-size: 16px; + font-size: 15px; font-weight: 600; letter-spacing: -0.02em; color: var(--text-strong); @@ -138,7 +64,7 @@ .card-sub { color: var(--muted); - font-size: 14px; + font-size: 13px; margin-top: 6px; line-height: 1.5; } @@ -148,10 +74,10 @@ =========================================== */ .stat { - background: color-mix(in srgb, var(--card) 96%, transparent); + background: var(--card); border-radius: var(--radius-md); padding: 14px 16px; - border: 1px solid color-mix(in srgb, var(--border) 92%, transparent); + border: 1px solid var(--border); transition: border-color var(--duration-normal) var(--ease-out), box-shadow var(--duration-normal) var(--ease-out); @@ -161,20 +87,20 @@ .stat:hover { border-color: var(--border-strong); box-shadow: - 0 6px 16px rgba(0, 0, 0, 0.18), + var(--shadow-sm), inset 0 1px 0 var(--card-highlight); } .stat-label { color: var(--muted); - font-size: 12px; + font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; } .stat-value { - font-size: 26px; + font-size: 24px; font-weight: 700; margin-top: 6px; letter-spacing: -0.03em; @@ -222,7 +148,7 @@ .account-count { margin-top: 10px; - font-size: 13px; + font-size: 12px; font-weight: 500; color: var(--muted); } @@ -258,13 +184,13 @@ .account-card-id { font-family: var(--mono); - font-size: 13px; + font-size: 12px; color: var(--muted); } .account-card-status { margin-top: 10px; - font-size: 14px; + font-size: 13px; } .account-card-status div { @@ -274,7 +200,7 @@ .account-card-error { margin-top: 8px; color: var(--danger); - font-size: 13px; + font-size: 12px; } /* =========================================== @@ -283,7 +209,7 @@ .label { color: var(--muted); - font-size: 13px; + font-size: 12px; font-weight: 500; } @@ -291,20 +217,17 @@ display: inline-flex; align-items: center; gap: 6px; - border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); + border: 1px solid var(--border); padding: 6px 12px; border-radius: var(--radius-full); - background: color-mix(in srgb, var(--secondary) 92%, transparent); - font-size: 14px; + background: var(--secondary); + font-size: 13px; font-weight: 500; - transition: - border-color var(--duration-fast) ease, - background var(--duration-fast) ease; + transition: border-color var(--duration-fast) ease; } .pill:hover { border-color: var(--border-strong); - background: var(--bg-hover); } .pill.danger { @@ -318,100 +241,67 @@ =========================================== */ .theme-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid var(--clay-border-color); - border-radius: 999px; - padding: 5px; - height: 36px; - background: var(--clay-bg); - overflow: hidden; - max-width: 36px; - transition: - max-width var(--clay-duration-normal) var(--clay-easing), - padding var(--clay-duration-normal) var(--clay-easing); + --theme-item: 28px; + --theme-gap: 2px; + --theme-pad: 4px; + position: relative; } -@media (hover: hover) { - .theme-toggle:hover { - max-width: 400px; - padding: 4px 6px; - } +.theme-toggle__track { + position: relative; + display: grid; + grid-template-columns: repeat(3, var(--theme-item)); + gap: var(--theme-gap); + padding: var(--theme-pad); + border-radius: var(--radius-full); + border: 1px solid var(--border); + background: var(--secondary); } -.theme-toggle:focus-within { - max-width: 400px; - padding: 4px 6px; +.theme-toggle__indicator { + position: absolute; + top: 50%; + left: var(--theme-pad); + width: var(--theme-item); + height: var(--theme-item); + border-radius: var(--radius-full); + transform: translateY(-50%) + translateX(calc(var(--theme-index, 0) * (var(--theme-item) + var(--theme-gap)))); + background: var(--accent); + transition: transform var(--duration-normal) var(--ease-out); + z-index: 0; } -.theme-toggle.theme-toggle--open { - max-width: 400px; - padding: 4px 6px; -} - -.theme-btn { +.theme-toggle__button { + height: var(--theme-item); + width: var(--theme-item); + display: grid; + place-items: center; border: 0; + border-radius: var(--radius-full); background: transparent; - padding: 6px 10px; - border-radius: 999px; - font-size: 0.84rem; color: var(--muted); - display: inline-flex; - align-items: center; - gap: 0.35rem; - white-space: nowrap; - flex-shrink: 0; cursor: pointer; - transition: - color var(--clay-duration-fast) var(--clay-easing), - background var(--clay-duration-fast) var(--clay-easing), - transform var(--clay-duration-fast) var(--clay-easing); + position: relative; + z-index: 1; + transition: color var(--duration-fast) ease; } -.theme-btn.active { - padding: 6px 8px; - background: var(--clay-bg-button); - color: var(--text); - box-shadow: var(--clay-shadow-pressed); -} - -.theme-btn:not(.active) { - opacity: 0; - pointer-events: none; - width: 0; - padding: 6px 0; - overflow: hidden; - transition: - opacity var(--clay-duration-fast) var(--clay-easing), - width var(--clay-duration-fast) var(--clay-easing), - padding var(--clay-duration-fast) var(--clay-easing), - color var(--clay-duration-fast) var(--clay-easing), - background var(--clay-duration-fast) var(--clay-easing), - transform var(--clay-duration-fast) var(--clay-easing); -} - -.theme-toggle:hover .theme-btn, -.theme-toggle:focus-within .theme-btn, -.theme-toggle--open .theme-btn { - opacity: 1; - pointer-events: auto; - width: auto; - padding: 6px 10px; -} - -.theme-btn:hover { - border: 0; +.theme-toggle__button:hover { color: var(--text); } -.theme-btn:active { - transform: scale(0.93); +.theme-toggle__button.active { + color: var(--accent-foreground); } -.theme-btn svg { - width: 16px; - height: 16px; +.theme-toggle__button.active .theme-icon { + stroke: var(--accent-foreground); +} + +.theme-icon { + width: 14px; + height: 14px; stroke: currentColor; fill: none; stroke-width: 1.5px; @@ -428,13 +318,13 @@ height: 8px; border-radius: var(--radius-full); background: var(--danger); - box-shadow: 0 0 8px color-mix(in srgb, var(--danger) 50%, transparent); + box-shadow: 0 0 8px rgba(239, 68, 68, 0.5); animation: pulse-subtle 2s ease-in-out infinite; } .statusDot.ok { background: var(--ok); - box-shadow: 0 0 8px color-mix(in srgb, var(--ok) 50%, transparent); + box-shadow: 0 0 8px rgba(34, 197, 94, 0.5); animation: none; } @@ -446,13 +336,12 @@ display: inline-flex; align-items: center; justify-content: center; - gap: 8px; - border: 1px solid color-mix(in srgb, var(--border) 92%, transparent); - background: color-mix(in srgb, var(--bg-elevated) 95%, transparent); - padding: 10px 18px; + border: 1px solid var(--border); + background: var(--bg-elevated); + padding: 9px 16px; border-radius: var(--radius-md); - font-size: 14px; + font-size: 13px; font-weight: 500; letter-spacing: -0.01em; cursor: pointer; @@ -463,14 +352,14 @@ transform var(--duration-fast) var(--ease-out); } -.btn:hover:not(:disabled) { +.btn:hover { background: var(--bg-hover); border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-sm); } -.btn:active:not(:disabled) { +.btn:active { background: var(--secondary); transform: translateY(0); box-shadow: none; @@ -488,16 +377,18 @@ } .btn.primary { - border-color: color-mix(in srgb, var(--accent) 88%, black 10%); + border-color: var(--accent); background: var(--accent); color: var(--primary-foreground); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.24); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } .btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); - box-shadow: var(--shadow-md); + box-shadow: + var(--shadow-md), + 0 0 20px var(--accent-glow); } /* Keyboard shortcut badge (shadcn style) */ @@ -521,20 +412,28 @@ background: rgba(255, 255, 255, 0.2); } +:root[data-theme="light"] .btn-kbd { + background: rgba(0, 0, 0, 0.08); +} + +:root[data-theme="light"] .btn.primary .btn-kbd { + background: rgba(255, 255, 255, 0.25); +} + .btn.active { - border-color: color-mix(in srgb, var(--accent) 35%, transparent); - background: color-mix(in srgb, var(--accent-subtle) 75%, var(--secondary)); + border-color: var(--accent); + background: var(--accent-subtle); color: var(--accent); } .btn.danger { - border-color: color-mix(in srgb, var(--danger) 25%, transparent); + border-color: transparent; background: var(--danger-subtle); color: var(--danger); } .btn.danger:hover { - background: color-mix(in srgb, var(--danger-subtle) 70%, transparent); + background: rgba(239, 68, 68, 0.15); } .btn--sm { @@ -542,16 +441,9 @@ font-size: 12px; } -.btn:focus-visible { - border-color: var(--ring); - box-shadow: var(--focus-ring); -} - .btn:disabled { opacity: 0.5; cursor: not-allowed; - transform: none; - box-shadow: none; } /* =========================================== @@ -569,39 +461,29 @@ .field span { color: var(--muted); - font-size: 14px; + font-size: 13px; font-weight: 500; } .field input, .field textarea, .field select { - border: 1px solid color-mix(in srgb, var(--input) 92%, transparent); - background: color-mix(in srgb, var(--card) 96%, var(--bg)); + border: 1px solid var(--input); + background: var(--card); border-radius: var(--radius-md); - padding: 10px 14px; + padding: 8px 12px; outline: none; box-shadow: inset 0 1px 0 var(--card-highlight); transition: border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - background var(--duration-fast) ease; + box-shadow var(--duration-fast) ease; } -.field input:focus-visible, -.field textarea:focus-visible, -.field select:focus-visible { +.field input:focus, +.field textarea:focus, +.field select:focus { border-color: var(--ring); box-shadow: var(--focus-ring); - background: var(--card); -} - -.field input:disabled, -.field textarea:disabled, -.field select:disabled { - opacity: 0.6; - cursor: not-allowed; - background: color-mix(in srgb, var(--secondary) 80%, transparent); } .field select { @@ -660,6 +542,12 @@ background: var(--bg-hover); } +:root[data-theme="light"] .btn.active { + border-color: var(--accent); + background: var(--accent-subtle); + color: var(--accent); +} + :root[data-theme="light"] .btn.primary { background: var(--accent); border-color: var(--accent); @@ -692,45 +580,23 @@ } .callout.danger { - border-color: color-mix(in srgb, var(--danger) 25%, transparent); - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--danger) 8%, transparent) 0%, - color-mix(in srgb, var(--danger) 4%, transparent) 100% - ); + border-color: rgba(239, 68, 68, 0.25); + background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(239, 68, 68, 0.04) 100%); color: var(--danger); } .callout.info { - border-color: color-mix(in srgb, var(--info) 25%, transparent); - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--info) 8%, transparent) 0%, - color-mix(in srgb, var(--info) 4%, transparent) 100% - ); + border-color: rgba(59, 130, 246, 0.25); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.04) 100%); color: var(--info); } .callout.success { - border-color: color-mix(in srgb, var(--ok) 25%, transparent); - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--ok) 8%, transparent) 0%, - color-mix(in srgb, var(--ok) 4%, transparent) 100% - ); + border-color: rgba(34, 197, 94, 0.25); + background: linear-gradient(135deg, rgba(34, 197, 94, 0.08) 0%, rgba(34, 197, 94, 0.04) 100%); color: var(--ok); } -.callout.warn { - border-color: color-mix(in srgb, var(--warn) 25%, transparent); - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--warn) 8%, transparent) 0%, - color-mix(in srgb, var(--warn) 4%, transparent) 100% - ); - color: var(--warn); -} - /* Compaction indicator */ .compaction-indicator { align-self: center; @@ -741,7 +607,7 @@ line-height: 1.2; padding: 6px 14px; margin-bottom: 8px; - border-radius: var(--radius-full); + border-radius: 999px; border: 1px solid var(--border); background: var(--panel-strong); color: var(--text); @@ -763,7 +629,7 @@ .compaction-indicator--active { color: var(--info); - border-color: color-mix(in srgb, var(--info) 35%, transparent); + border-color: rgba(59, 130, 246, 0.35); } .compaction-indicator--active svg { @@ -772,17 +638,17 @@ .compaction-indicator--complete { color: var(--ok); - border-color: color-mix(in srgb, var(--ok) 35%, transparent); + border-color: rgba(34, 197, 94, 0.35); } .compaction-indicator--fallback { - color: var(--warn); + color: #d97706; border-color: rgba(217, 119, 6, 0.35); } .compaction-indicator--fallback-cleared { color: var(--ok); - border-color: color-mix(in srgb, var(--ok) 35%, transparent); + border-color: rgba(34, 197, 94, 0.35); } @keyframes compaction-spin { @@ -808,6 +674,13 @@ max-width: 100%; } +:root[data-theme="light"] .code-block, +:root[data-theme="light"] .list-item, +:root[data-theme="light"] .table-row, +:root[data-theme="light"] .chip { + background: var(--bg); +} + /* =========================================== Lists =========================================== */ @@ -818,24 +691,16 @@ container-type: inline-size; } -.list-scroll { - max-height: 400px; - overflow-y: auto; -} - .list-item { display: grid; grid-template-columns: minmax(0, 1fr) minmax(200px, 260px); gap: 16px; align-items: start; - border: 1px solid color-mix(in srgb, var(--border) 92%, transparent); + border: 1px solid var(--border); border-radius: var(--radius-md); padding: 12px; - background: color-mix(in srgb, var(--card) 97%, transparent); - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - background var(--duration-fast) ease; + background: var(--card); + transition: border-color var(--duration-fast) ease; } .list-item-clickable { @@ -844,14 +709,11 @@ .list-item-clickable:hover { border-color: var(--border-strong); - background: color-mix(in srgb, var(--card) 80%, var(--bg-hover)); - box-shadow: var(--shadow-sm); } .list-item-selected { - border-color: color-mix(in srgb, var(--accent) 35%, transparent); + border-color: var(--accent); box-shadow: var(--focus-ring); - background: color-mix(in srgb, var(--accent-subtle) 45%, var(--card)); } .list-main { @@ -866,9 +728,7 @@ .list-sub { color: var(--muted); - font-size: 13px; - overflow-wrap: anywhere; - word-break: break-word; + font-size: 12px; } .list-meta { @@ -900,7 +760,7 @@ .cron-job .list-title { font-weight: 600; - font-size: 16px; + font-size: 15px; letter-spacing: -0.015em; } @@ -940,7 +800,6 @@ display: grid; gap: 3px; margin-top: 2px; - min-width: 0; } .cron-job-detail-label { @@ -954,9 +813,6 @@ .cron-job-detail-value { font-size: 13px; line-height: 1.35; - overflow-wrap: anywhere; - word-break: break-word; - min-width: 0; } .cron-job-state { @@ -996,7 +852,7 @@ .cron-job-status-ok { color: var(--ok); - border-color: color-mix(in srgb, var(--ok) 35%, transparent); + border-color: rgba(34, 197, 94, 0.35); background: var(--ok-subtle); } @@ -1065,13 +921,13 @@ } .chip { - font-size: 13px; + font-size: 12px; font-weight: 500; - border: 1px solid color-mix(in srgb, var(--border) 85%, transparent); + border: 1px solid var(--border); border-radius: var(--radius-full); padding: 5px 12px; color: var(--muted); - background: color-mix(in srgb, var(--secondary) 92%, transparent); + background: var(--secondary); transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out), @@ -1080,7 +936,6 @@ .chip:hover { border-color: var(--border-strong); - background: var(--bg-hover); transform: translateY(-1px); } @@ -1102,7 +957,7 @@ .chip-danger { color: var(--danger); - border-color: color-mix(in srgb, var(--danger) 30%, transparent); + border-color: rgba(239, 68, 68, 0.3); background: var(--danger-subtle); } @@ -1112,7 +967,7 @@ .table { display: grid; - gap: 8px; + gap: 6px; } .table-head, @@ -1124,32 +979,22 @@ } .table-head { - font-size: 13px; + font-size: 12px; font-weight: 500; color: var(--muted); padding: 0 12px; } .table-row { - border: 1px solid color-mix(in srgb, var(--border) 92%, transparent); - padding: 12px 14px; + border: 1px solid var(--border); + padding: 10px 12px; border-radius: var(--radius-md); - background: color-mix(in srgb, var(--card) 97%, transparent); - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - background var(--duration-fast) ease; + background: var(--card); + transition: border-color var(--duration-fast) ease; } .table-row:hover { border-color: var(--border-strong); - background: color-mix(in srgb, var(--card) 82%, var(--bg-hover)); - box-shadow: var(--shadow-sm); -} - -.table-row:focus-within { - border-color: var(--ring); - box-shadow: var(--focus-ring); } .session-link { @@ -1183,13 +1028,12 @@ =========================================== */ .log-stream { - border: 1px solid color-mix(in srgb, var(--border) 92%, transparent); + border: 1px solid var(--border); border-radius: var(--radius-md); - background: color-mix(in srgb, var(--card) 98%, transparent); + background: var(--card); max-height: 500px; overflow: auto; container-type: inline-size; - box-shadow: inset 0 1px 0 var(--card-highlight); } .log-row { @@ -1197,9 +1041,9 @@ grid-template-columns: 90px 70px minmax(140px, 200px) minmax(0, 1fr); gap: 12px; align-items: start; - padding: 9px 12px; - border-bottom: 1px solid color-mix(in srgb, var(--border) 90%, transparent); - font-size: 13px; + padding: 8px 12px; + border-bottom: 1px solid var(--border); + font-size: 12px; transition: background var(--duration-fast) ease; } @@ -1401,7 +1245,7 @@ .chat-new-messages { align-self: center; margin: 8px auto 0; - border-radius: var(--radius-full); + border-radius: 999px; padding: 6px 12px; font-size: 12px; line-height: 1; @@ -1440,16 +1284,31 @@ min-width: 0; } +:root[data-theme="light"] .chat-bubble { + border-color: var(--border); + background: var(--bg); +} + .chat-line.user .chat-bubble { border-color: transparent; background: var(--accent-subtle); } +:root[data-theme="light"] .chat-line.user .chat-bubble { + border-color: rgba(234, 88, 12, 0.2); + background: rgba(251, 146, 60, 0.12); +} + .chat-line.assistant .chat-bubble { border-color: transparent; background: var(--secondary); } +:root[data-theme="light"] .chat-line.assistant .chat-bubble { + border-color: var(--border); + background: var(--bg-muted); +} + @keyframes chatStreamPulse { 0%, 100% { @@ -1580,6 +1439,10 @@ background: var(--secondary); } +:root[data-theme="light"] .chat-text :where(:not(pre) > code) { + background: var(--bg-muted); +} + .chat-text :where(pre) { margin-top: 0.75em; padding: 10px 12px; @@ -1589,6 +1452,10 @@ overflow: auto; } +:root[data-theme="light"] .chat-text :where(pre) { + background: var(--bg-muted); +} + .chat-text :where(pre code) { font-size: 12px; white-space: pre; @@ -1625,6 +1492,10 @@ gap: 4px; } +:root[data-theme="light"] .chat-tool-card { + background: var(--bg-muted); +} + .chat-tool-card__title { font-family: var(--mono); font-size: 12px; @@ -1679,8 +1550,12 @@ background: var(--card); } +:root[data-theme="light"] .chat-tool-card__output { + background: var(--bg); +} + .chat-stamp { - font-size: 12px; + font-size: 11px; color: var(--muted); } @@ -1810,7 +1685,7 @@ } .exec-approval-title { - font-size: 15px; + font-size: 14px; font-weight: 600; } @@ -1887,8 +1762,6 @@ display: grid; gap: 12px; align-self: start; - position: sticky; - top: 16px; } .agents-main { @@ -1929,7 +1802,7 @@ width: 32px; height: 32px; border-radius: 50%; - background: hsl(var(--agent-hue, 220) 30% 18%); + background: var(--secondary); display: grid; place-items: center; font-weight: 600; @@ -2017,13 +1890,6 @@ color: white; } -.agent-tab-count { - font-weight: 400; - font-size: 11px; - opacity: 0.7; - margin-left: 4px; -} - .agents-overview-grid { display: grid; gap: 14px; @@ -2034,10 +1900,6 @@ display: grid; gap: 6px; min-width: 0; - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 12px; - background: var(--bg-elevated); } .agent-kv > div { @@ -2287,731 +2149,3 @@ grid-template-columns: 1fr; } } - -.agent-identity-card { - display: flex; - gap: 16px; - align-items: center; - padding: 16px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--bg-elevated); -} - -.agent-identity-card .agent-avatar { - width: 56px; - height: 56px; - font-size: 24px; - flex-shrink: 0; -} - -.agent-identity-details { - display: grid; - gap: 4px; - min-width: 0; -} - -.agent-identity-name { - font-weight: 700; - font-size: 16px; -} - -.agent-identity-meta { - display: flex; - gap: 8px; - align-items: center; - flex-wrap: wrap; - color: var(--muted); - font-size: 13px; -} - -.agent-chip-input { - display: flex; - flex-wrap: wrap; - gap: 6px; - align-items: center; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--card); - padding: 6px 8px; - min-height: 38px; - cursor: text; - transition: border-color var(--duration-fast) ease; -} - -.agent-chip-input:focus-within { - border-color: var(--accent); -} - -.agent-chip-input .chip { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.agent-chip-input .chip-remove { - cursor: pointer; - opacity: 0.6; - font-size: 14px; - line-height: 1; - padding: 0 2px; - background: none; - border: none; - color: inherit; -} - -.agent-chip-input .chip-remove:hover { - opacity: 1; -} - -.agent-chip-input input { - border: none; - background: transparent; - color: inherit; - font: inherit; - font-size: 13px; - outline: none; - padding: 2px 0; - flex: 1; - min-width: 120px; -} - -.agent-actions-wrap { - position: relative; -} - -.agent-actions-toggle { - background: var(--secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 6px 10px; - cursor: pointer; - font-size: 16px; - line-height: 1; - color: var(--muted); - transition: border-color var(--duration-fast) ease; -} - -.agent-actions-toggle:hover { - border-color: var(--border-strong); - color: var(--vscode-text); -} - -.agent-actions-menu { - position: absolute; - top: calc(100% + 6px); - right: 0; - z-index: 50; - min-width: 180px; - background: var(--glass-bg-elevated); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--glass-shadow-md); - padding: 4px; - display: grid; - gap: 2px; -} - -.agent-actions-menu button { - display: block; - width: 100%; - text-align: left; - padding: 8px 12px; - border: none; - background: transparent; - color: var(--vscode-text); - font-size: 13px; - border-radius: var(--radius-sm); - cursor: pointer; - transition: background var(--duration-fast) ease; -} - -.agent-actions-menu button:hover { - background: var(--vscode-hover); -} - -.agent-actions-menu button:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.agent-actions-menu button:disabled:hover { - background: transparent; -} - -.workspace-link { - background: none; - border: none; - color: var(--accent); - cursor: pointer; - font: inherit; - padding: 0; - text-decoration: underline; - text-decoration-style: dotted; - text-underline-offset: 3px; -} - -.workspace-link:hover { - text-decoration-style: solid; -} - -/* =========================================== - Overview Dashboard Cards - =========================================== */ - -.ov-cards { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; - margin-top: 18px; -} - -.ov-stat-card { - --ov-accent: var(--muted); - display: grid; - gap: 0; - padding: 0; - overflow: hidden; - border-top: 2px solid var(--ov-accent); - position: relative; -} - -.ov-stat-card.clickable { - cursor: pointer; - transition: - border-color 0.15s ease, - transform 0.15s ease, - box-shadow 0.15s ease; -} - -.ov-stat-card.clickable:hover { - border-color: var(--accent); - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); -} - -.ov-stat-card[data-kind="cost"] { - --ov-accent: var(--kn-bioluminescence); -} - -.ov-stat-card[data-kind="sessions"] { - --ov-accent: var(--kn-silver); -} - -.ov-stat-card[data-kind="skills"] { - --ov-accent: var(--kn-claw-ember); -} - -.ov-stat-card[data-kind="cron"] { - --ov-accent: var(--vscode-accent); -} - -.ov-stat-card__inner { - display: flex; - gap: 12px; - align-items: flex-start; - padding: 14px 16px; -} - -.ov-stat-card__icon { - flex-shrink: 0; - width: 20px; - height: 20px; - color: var(--ov-accent); - opacity: 0.8; - margin-top: 1px; -} - -.ov-stat-card__icon svg { - width: 100%; - height: 100%; -} - -.ov-stat-card__body { - min-width: 0; - flex: 1; -} - -.ov-stat-card__body .stat-label { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--muted); - margin-bottom: 6px; - font-weight: 600; -} - -.ov-stat-card__body .stat-value { - font-size: 22px; - font-weight: 700; - letter-spacing: -0.02em; - line-height: 1.1; -} - -.ov-stat-card__body .muted { - font-size: 12px; - margin-top: 6px; - line-height: 1.4; -} - -.redacted { - filter: blur(5px); - user-select: none; - pointer-events: none; - transition: filter var(--duration-normal, 250ms) ease; -} - -/* Recent sessions */ - -.ov-recent-sessions { - margin-top: 14px; -} - -.ov-session-list { - margin-top: 10px; -} - -.ov-session-row { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); - font-size: 13px; - transition: opacity 0.1s ease; -} - -.ov-session-row:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.ov-session-row:first-child { - padding-top: 0; -} - -.ov-session-key { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-weight: 500; -} - -.ov-session-key .blur-digits { - filter: blur(5px); - transition: filter 200ms ease-out; - user-select: none; -} - -.ov-session-row:hover .blur-digits { - filter: none; -} - -/* =========================================== - Attention Center - =========================================== */ - -.ov-attention { - margin-top: 18px; -} - -.ov-attention-list { - margin-top: 12px; - display: flex; - flex-direction: column; - gap: 8px; -} - -.ov-attention-item { - display: flex; - align-items: flex-start; - gap: 10px; - padding: 10px 12px; - border-radius: var(--radius); - border: 1px solid var(--border); - font-size: 13px; -} - -.ov-attention-item.danger { - border-color: var(--danger); - background: var(--danger-subtle); -} - -.ov-attention-item.warn { - border-color: var(--warn, #d97706); - background: color-mix(in srgb, var(--warn, #d97706) 8%, transparent); -} - -.ov-attention-icon { - flex-shrink: 0; - width: 16px; - height: 16px; - margin-top: 1px; -} - -.ov-attention-icon svg { - width: 100%; - height: 100%; -} - -.ov-attention-body { - flex: 1; - min-width: 0; -} - -.ov-attention-title { - font-weight: 600; - margin-bottom: 2px; -} - -.ov-attention-link { - flex-shrink: 0; - font-size: 12px; - color: var(--accent); - text-decoration: none; - align-self: center; -} - -.ov-attention-link:hover { - text-decoration: underline; -} - -/* =========================================== - Overview Event Log - =========================================== */ - -.ov-event-log { - margin-top: 0; -} - -.ov-expandable-toggle { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 14px; - font-weight: 600; - list-style: none; - padding: 0; -} - -.ov-expandable-toggle::-webkit-details-marker { - display: none; -} - -.ov-expandable-toggle .nav-item__icon { - width: 16px; - height: 16px; -} - -.ov-expandable-toggle .nav-item__icon svg { - width: 100%; - height: 100%; -} - -.ov-count-badge { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 20px; - height: 20px; - padding: 0 6px; - border-radius: 10px; - background: var(--border); - color: var(--muted); - font-size: 11px; - font-weight: 600; -} - -.ov-event-log-list { - margin-top: 12px; - max-height: 300px; - overflow-y: auto; -} - -.ov-event-log-entry { - display: flex; - align-items: baseline; - gap: 8px; - padding: 4px 0; - border-bottom: 1px solid var(--border); - font-size: 12px; - font-family: var(--mono); -} - -.ov-event-log-entry:last-child { - border-bottom: none; -} - -.ov-event-log-ts { - flex-shrink: 0; - color: var(--muted); - width: 70px; -} - -.ov-event-log-name { - font-weight: 600; - min-width: 100px; -} - -.ov-event-log-payload { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* =========================================== - Overview Log Tail - =========================================== */ - -.ov-log-tail { - margin-top: 0; -} - -.ov-log-refresh { - margin-left: auto; - cursor: pointer; - width: 14px; - height: 14px; - color: var(--muted); -} - -.ov-log-refresh svg { - width: 100%; - height: 100%; -} - -.ov-log-refresh:hover { - color: var(--fg); -} - -.ov-log-tail-content { - margin-top: 12px; - max-height: 250px; - overflow: auto; - font-family: var(--mono); - font-size: 11px; - line-height: 1.6; - white-space: pre-wrap; - word-break: break-all; - background: var(--bg-inset, var(--bg)); - padding: 12px; - border-radius: var(--radius); - border: 1px solid var(--border); -} - -/* =========================================== - Overview Quick Actions - =========================================== */ - -.ov-quick-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 18px; -} - -.ov-quick-action-btn { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 13px; -} - -.ov-quick-action-btn .nav-item__icon { - width: 14px; - height: 14px; -} - -.ov-quick-action-btn .nav-item__icon svg { - width: 100%; - height: 100%; -} - -/* =========================================== - Stream Mode Banner - =========================================== */ - -.ov-stream-banner { - display: flex; - align-items: center; - gap: 8px; -} - -.ov-stream-banner .nav-item__icon { - width: 14px; - height: 14px; -} - -.ov-stream-banner .nav-item__icon svg { - width: 100%; - height: 100%; -} - -/* =========================================== - Overview Bottom Grid - =========================================== */ - -.ov-bottom-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 14px; -} - -@media (max-width: 768px) { - .ov-bottom-grid { - grid-template-columns: 1fr; - } - - .ov-cards { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 480px) { - .ov-cards { - grid-template-columns: 1fr; - } -} - -/* =========================================== - Command Palette - =========================================== */ - -.cmd-palette-overlay { - position: fixed; - inset: 0; - z-index: 1000; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: min(20vh, 160px); - animation: fade-in 0.12s ease-out; -} - -.cmd-palette { - width: min(560px, 90vw); - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - overflow: hidden; - animation: scale-in 0.15s ease-out; -} - -.cmd-palette__input { - width: 100%; - padding: 14px 18px; - background: transparent; - border: none; - border-bottom: 1px solid var(--border); - font-size: 15px; - color: var(--fg); - outline: none; -} - -.cmd-palette__input::placeholder { - color: var(--muted); -} - -.cmd-palette__results { - max-height: 320px; - overflow-y: auto; - padding: 6px 0; -} - -.cmd-palette__group-label { - padding: 8px 18px 4px; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--muted); - font-weight: 600; -} - -.cmd-palette__item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 18px; - cursor: pointer; - font-size: 14px; - transition: background 0.1s; -} - -.cmd-palette__item:hover, -.cmd-palette__item--active { - background: var(--hover); -} - -.cmd-palette__item .nav-item__icon { - width: 16px; - height: 16px; - flex-shrink: 0; -} - -.cmd-palette__item .nav-item__icon svg { - width: 100%; - height: 100%; -} - -.cmd-palette__item-desc { - margin-left: auto; - font-size: 12px; -} - -/* =========================================== - Bottom Tabs (Mobile Navigation) - =========================================== */ - -.bottom-tabs { - display: none; - border-top: 1px solid var(--border); - background: var(--card); - padding: 4px 0; -} - -.bottom-tab { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - flex: 1; - padding: 6px 4px; - border: none; - background: none; - color: var(--muted); - cursor: pointer; - font-size: 10px; - transition: color 0.15s; -} - -.bottom-tab--active { - color: var(--accent); -} - -.bottom-tab__icon { - width: 20px; - height: 20px; -} - -.bottom-tab__icon svg { - width: 100%; - height: 100%; -} - -.bottom-tab__label { - font-weight: 500; -} - -@media (max-width: 768px) { - .bottom-tabs { - display: flex; - } -} diff --git a/ui/src/styles/config.css b/ui/src/styles/config.css index e5ef45bc56b..ec4003a1244 100644 --- a/ui/src/styles/config.css +++ b/ui/src/styles/config.css @@ -27,6 +27,10 @@ overflow: hidden; } +:root[data-theme="light"] .config-sidebar { + background: var(--bg-hover); +} + .config-sidebar__header { display: flex; align-items: center; @@ -37,7 +41,7 @@ .config-sidebar__title { font-weight: 600; - font-size: 15px; + font-size: 14px; letter-spacing: -0.01em; } @@ -71,7 +75,7 @@ border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-elevated); - font-size: 14px; + font-size: 13px; outline: none; transition: border-color var(--duration-fast) ease, @@ -89,6 +93,14 @@ background: var(--bg-hover); } +:root[data-theme="light"] .config-search__input { + background: white; +} + +:root[data-theme="light"] .config-search__input:focus { + background: white; +} + .config-search__clear { position: absolute; right: 22px; @@ -133,7 +145,7 @@ border-radius: var(--radius-md); background: transparent; color: var(--muted); - font-size: 14px; + font-size: 13px; font-weight: 500; text-align: left; cursor: pointer; @@ -147,6 +159,10 @@ color: var(--text); } +:root[data-theme="light"] .config-nav__item:hover { + background: rgba(0, 0, 0, 0.04); +} + .config-nav__item.active { background: var(--accent-subtle); color: var(--accent); @@ -190,6 +206,10 @@ border: 1px solid var(--border); } +:root[data-theme="light"] .config-mode-toggle { + background: white; +} + .config-mode-toggle__btn { flex: 1; padding: 9px 14px; @@ -240,6 +260,10 @@ border-bottom: 1px solid var(--border); } +:root[data-theme="light"] .config-actions { + background: var(--bg-hover); +} + .config-actions__left, .config-actions__right { display: flex; @@ -251,7 +275,7 @@ padding: 6px 14px; border-radius: var(--radius-full); background: var(--accent-subtle); - border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent); + border: 1px solid rgba(255, 77, 77, 0.3); color: var(--accent); font-size: 12px; font-weight: 600; @@ -265,7 +289,7 @@ /* Diff Panel */ .config-diff { margin: 18px 22px 0; - border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent); + border: 1px solid rgba(255, 77, 77, 0.25); border-radius: var(--radius-lg); background: var(--accent-subtle); overflow: hidden; @@ -319,6 +343,10 @@ font-family: var(--mono); } +:root[data-theme="light"] .config-diff__item { + background: white; +} + .config-diff__path { font-weight: 600; color: var(--text); @@ -356,6 +384,10 @@ background: var(--bg-accent); } +:root[data-theme="light"] .config-section-hero { + background: var(--bg-hover); +} + .config-section-hero__icon { width: 30px; height: 30px; @@ -379,7 +411,7 @@ } .config-section-hero__title { - font-size: 17px; + font-size: 16px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; @@ -388,7 +420,7 @@ } .config-section-hero__desc { - font-size: 14px; + font-size: 13px; color: var(--muted); } @@ -402,6 +434,10 @@ overflow-x: auto; } +:root[data-theme="light"] .config-subnav { + background: var(--bg-hover); +} + .config-subnav__item { border: 1px solid transparent; border-radius: var(--radius-full); @@ -418,6 +454,10 @@ white-space: nowrap; } +:root[data-theme="light"] .config-subnav__item { + background: white; +} + .config-subnav__item:hover { color: var(--text); border-color: var(--border); @@ -511,6 +551,10 @@ border-color: var(--border-strong); } +:root[data-theme="light"] .config-section-card { + background: white; +} + .config-section-card__header { display: flex; align-items: flex-start; @@ -520,6 +564,10 @@ border-bottom: 1px solid var(--border); } +:root[data-theme="light"] .config-section-card__header { + background: var(--bg-hover); +} + .config-section-card__icon { width: 34px; height: 34px; @@ -539,7 +587,7 @@ .config-section-card__title { margin: 0; - font-size: 18px; + font-size: 17px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; @@ -549,7 +597,7 @@ .config-section-card__desc { margin: 5px 0 0; - font-size: 14px; + font-size: 13px; color: var(--muted); line-height: 1.45; } @@ -576,23 +624,23 @@ padding: 14px; border-radius: var(--radius-md); background: var(--danger-subtle); - border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent); + border: 1px solid rgba(239, 68, 68, 0.3); } .cfg-field__label { - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--text); } .cfg-field__help { - font-size: 13px; + font-size: 12px; color: var(--muted); line-height: 1.45; } .cfg-field__error { - font-size: 13px; + font-size: 12px; color: var(--danger); } @@ -627,6 +675,14 @@ background: var(--bg-hover); } +:root[data-theme="light"] .cfg-input { + background: white; +} + +:root[data-theme="light"] .cfg-input:focus { + background: white; +} + .cfg-input--sm { padding: 9px 12px; font-size: 13px; @@ -677,6 +733,10 @@ box-shadow: var(--focus-ring); } +:root[data-theme="light"] .cfg-textarea { + background: white; +} + .cfg-textarea--sm { padding: 10px 12px; font-size: 12px; @@ -691,6 +751,10 @@ background: var(--bg-accent); } +:root[data-theme="light"] .cfg-number { + background: white; +} + .cfg-number__btn { width: 44px; border: none; @@ -711,6 +775,14 @@ cursor: not-allowed; } +:root[data-theme="light"] .cfg-number__btn { + background: var(--bg-hover); +} + +:root[data-theme="light"] .cfg-number__btn:hover:not(:disabled) { + background: var(--border); +} + .cfg-number__input { width: 85px; padding: 11px; @@ -753,6 +825,10 @@ box-shadow: var(--focus-ring); } +:root[data-theme="light"] .cfg-select { + background-color: white; +} + /* Segmented Control */ .cfg-segmented { display: inline-flex; @@ -762,13 +838,17 @@ background: var(--bg-accent); } +:root[data-theme="light"] .cfg-segmented { + background: var(--bg-hover); +} + .cfg-segmented__btn { padding: 9px 18px; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--muted); - font-size: 14px; + font-size: 13px; font-weight: 500; cursor: pointer; transition: @@ -818,6 +898,14 @@ cursor: not-allowed; } +:root[data-theme="light"] .cfg-toggle-row { + background: white; +} + +:root[data-theme="light"] .cfg-toggle-row:hover:not(.disabled) { + background: var(--bg-hover); +} + .cfg-toggle-row__content { flex: 1; min-width: 0; @@ -825,7 +913,7 @@ .cfg-toggle-row__label { display: block; - font-size: 15px; + font-size: 14px; font-weight: 500; color: var(--text); } @@ -833,7 +921,7 @@ .cfg-toggle-row__help { display: block; margin-top: 3px; - font-size: 13px; + font-size: 12px; color: var(--muted); line-height: 1.45; } @@ -864,6 +952,10 @@ border-color var(--duration-normal) ease; } +:root[data-theme="light"] .cfg-toggle__track { + background: var(--border); +} + .cfg-toggle__track::after { content: ""; position: absolute; @@ -881,7 +973,7 @@ .cfg-toggle input:checked + .cfg-toggle__track { background: var(--ok-subtle); - border-color: color-mix(in srgb, var(--ok) 40%, transparent); + border-color: rgba(34, 197, 94, 0.4); } .cfg-toggle input:checked + .cfg-toggle__track::after { @@ -901,6 +993,10 @@ overflow: hidden; } +:root[data-theme="light"] .cfg-object { + background: white; +} + .cfg-object__header { display: flex; align-items: center; @@ -970,6 +1066,10 @@ border-bottom: 1px solid var(--border); } +:root[data-theme="light"] .cfg-array__header { + background: var(--bg-hover); +} + .cfg-array__label { flex: 1; font-size: 14px; @@ -985,6 +1085,10 @@ border-radius: var(--radius-full); } +:root[data-theme="light"] .cfg-array__count { + background: white; +} + .cfg-array__add { display: inline-flex; align-items: center; @@ -1052,6 +1156,10 @@ border-bottom: 1px solid var(--border); } +:root[data-theme="light"] .cfg-array__item-header { + background: var(--bg-hover); +} + .cfg-array__item-index { font-size: 11px; font-weight: 600; @@ -1112,6 +1220,10 @@ border-bottom: 1px solid var(--border); } +:root[data-theme="light"] .cfg-map__header { + background: var(--bg-hover); +} + .cfg-map__label { font-size: 13px; font-weight: 600; @@ -1208,7 +1320,7 @@ } .pill--ok { - border-color: color-mix(in srgb, var(--ok) 35%, transparent); + border-color: rgba(34, 197, 94, 0.35); color: var(--ok); } @@ -1332,85 +1444,3 @@ min-width: 70px; } } - -/* =========================================== - Environment Values Blur + Peek Toggle - =========================================== */ - -.config-env-values--blurred .cfg-input, -.config-env-values--blurred .cfg-number__input, -.config-env-values--blurred textarea { - color: transparent; - text-shadow: 0 0 8px var(--text); -} - -.config-env-values--blurred .cfg-input::placeholder, -.config-env-values--blurred textarea::placeholder { - text-shadow: none; - color: var(--muted); - opacity: 0.7; -} - -.config-env-values--blurred .cfg-input:focus, -.config-env-values--blurred .cfg-number__input:focus, -.config-env-values--blurred textarea:focus { - color: transparent; - text-shadow: 0 0 8px var(--text); -} - -.config-env-values--visible.config-env-values--blurred .cfg-input, -.config-env-values--visible.config-env-values--blurred .cfg-number__input, -.config-env-values--visible.config-env-values--blurred textarea { - color: var(--text); - text-shadow: none; -} - -.config-env-values--visible.config-env-values--blurred .cfg-input:focus, -.config-env-values--visible.config-env-values--blurred .cfg-number__input:focus, -.config-env-values--visible.config-env-values--blurred textarea:focus { - color: var(--text); - text-shadow: none; -} - -.config-env-peek-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - border-radius: var(--radius-md); - border: 1px solid var(--border); - background: transparent; - color: var(--muted); - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: all var(--duration-fast) ease; - flex-shrink: 0; - margin-left: auto; -} - -.config-env-peek-btn:hover { - color: var(--text); - border-color: var(--border-strong); - background: var(--bg-hover); -} - -.config-env-peek-btn--active { - color: var(--accent); - border-color: color-mix(in srgb, var(--accent) 40%, transparent); - background: color-mix(in srgb, var(--accent) 8%, transparent); -} - -.config-env-peek-btn svg { - flex-shrink: 0; -} - -/* Raw JSON redaction blur */ - -.config-raw-redacted { - color: transparent !important; - text-shadow: 0 0 8px var(--text); - transition: - color var(--duration-normal, 250ms) ease, - text-shadow var(--duration-normal, 250ms) ease; -} diff --git a/ui/src/styles/glass.css b/ui/src/styles/glass.css deleted file mode 100644 index e059a72b691..00000000000 --- a/ui/src/styles/glass.css +++ /dev/null @@ -1,554 +0,0 @@ -/* ════════════════════════════════════════════════════════ - Glass Component System - Glassmorphism primitives used across dashboard views. - ════════════════════════════════════════════════════════ */ - -/* ─── Animations ─── */ - -@keyframes glass-enter { - from { - opacity: 0; - transform: scale(0.97) translateY(6px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -@keyframes modal-overlay-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes modal-dialog-in { - from { - opacity: 0; - transform: scale(0.95) translateY(12px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -@keyframes glass-dropdown-in { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes ambient-drift { - 0% { - background-position: 0% 0%; - } - 50% { - background-position: 100% 100%; - } - 100% { - background-position: 0% 0%; - } -} - -@keyframes active-breathe { - 0%, - 100% { - opacity: 0.5; - } - 50% { - opacity: 1; - } -} - -@keyframes card-rise { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.glass-animate-in { - animation: glass-enter var(--clay-duration-normal) var(--clay-easing) both; -} - -/* ─── Glass Buttons ─── */ - -.glass-btn-primary { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.4rem; - padding: 10px 18px; - border: none; - border-radius: var(--radius-sm); - background: linear-gradient(135deg, var(--kn-claw), var(--kn-claw-deep)); - color: #fff; - font-weight: 600; - font-size: 0.88rem; - cursor: pointer; - position: relative; - overflow: hidden; - transition: - transform 0.15s ease, - box-shadow 0.2s ease, - filter 0.15s ease; -} - -.glass-btn-primary:hover { - transform: translateY(-1px); - filter: brightness(1.1); - box-shadow: 0 4px 16px rgba(202, 58, 41, 0.3); -} - -.glass-btn-primary:active { - transform: translateY(0); -} - -.glass-btn-secondary { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.4rem; - padding: 10px 18px; - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - color: var(--text); - font-weight: 500; - font-size: 0.88rem; - cursor: pointer; - transition: - border-color 0.2s ease, - background 0.15s ease; -} - -.glass-btn-secondary:hover { - border-color: var(--glass-border-hover); - background: var(--bg-hover); -} - -.glass-btn-ocean { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.4rem; - padding: 10px 18px; - border: 1px solid rgba(0, 212, 170, 0.2); - border-radius: var(--radius-sm); - background: rgba(0, 212, 170, 0.08); - color: var(--kn-bioluminescence); - font-weight: 600; - font-size: 0.88rem; - cursor: pointer; - transition: - border-color 0.2s ease, - background 0.15s ease; -} - -.glass-btn-ocean:hover { - border-color: rgba(0, 212, 170, 0.35); - background: rgba(0, 212, 170, 0.14); -} - -/* ─── Glass Input ─── */ - -.glass-input { - width: 100%; - padding: 10px 14px; - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - background: var(--glass-bg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - color: var(--text); - font-size: 0.92rem; - font-family: inherit; - transition: - border-color 0.2s ease, - box-shadow 0.2s ease; -} - -.glass-input:focus { - outline: none; - border-color: var(--accent); - border-width: 2px; - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.glass-input::placeholder { - color: var(--muted); -} - -/* ─── Glass Tabs ─── */ - -.glass-tab { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 6px 14px; - border: none; - border-radius: var(--radius-sm); - background: transparent; - color: var(--muted); - font-size: 0.82rem; - font-weight: 500; - cursor: pointer; - position: relative; - transition: - color 0.15s ease, - background 0.15s ease; -} - -.glass-tab:hover { - color: var(--text); - background: var(--accent-subtle); -} - -.glass-tab-active { - color: var(--text); - background: var(--accent-subtle); - font-weight: 600; -} - -.glass-tab-active::after { - content: ""; - position: absolute; - bottom: 0; - left: 20%; - width: 60%; - height: 2px; - background: linear-gradient(90deg, transparent, var(--accent), transparent); - border-radius: 1px; -} - -.glass-segmented-control { - display: inline-flex; - align-items: center; - gap: 2px; - padding: 3px; - border: 1px solid var(--glass-border); - border-radius: var(--radius-full); - background: var(--glass-bg); -} - -/* ─── Glass Dialog ─── */ - -.glass-dialog { - background: var(--glass-bg-elevated); - backdrop-filter: blur(40px) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(40px) saturate(var(--glass-saturate)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - position: relative; - overflow: hidden; -} - -/* ─── Glass Select Panel (Dropdown) ─── */ - -.glass-select-panel { - background: var(--glass-bg-elevated); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - animation: glass-dropdown-in 0.15s ease-out both; -} - -/* ─── Glass Overlay (Modal Backdrop) ─── */ - -.glass-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - z-index: 100; - animation: modal-overlay-in 0.25s ease-out both; -} - -/* ─── Glass Depth Layers ─── */ - -.glass-layer-1 { - background: var(--glass-bg); - backdrop-filter: blur(8px) saturate(120%); - -webkit-backdrop-filter: blur(8px) saturate(120%); -} - -.glass-layer-2 { - background: var(--glass-bg-elevated); - backdrop-filter: blur(16px) saturate(140%); - -webkit-backdrop-filter: blur(16px) saturate(140%); -} - -.glass-layer-3 { - background: rgba(0, 0, 0, 0.3); - backdrop-filter: blur(32px) saturate(160%); - -webkit-backdrop-filter: blur(32px) saturate(160%); -} - -/* ─── Glass Card Variants ─── */ - -.glass-card { - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-sm); - transition: - border-color 0.2s ease, - box-shadow 0.2s ease; -} - -.glass-card:hover { - border-color: var(--glass-border-hover); - box-shadow: var(--shadow-md); -} - -.glass-card-active { - border-color: var(--accent); - box-shadow: - 0 0 0 1px var(--accent), - var(--shadow-md); -} - -.glass-card-active-ocean { - border-color: var(--kn-bioluminescence); - box-shadow: - 0 0 0 1px var(--kn-bioluminescence), - var(--shadow-md); -} - -/* ─── Glass Noise Texture ─── */ - -.glass-noise::after { - content: ""; - position: absolute; - inset: 0; - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); - opacity: 0.05; - mix-blend-mode: overlay; - pointer-events: none; - border-radius: inherit; -} - -/* ─── Glass Border Gradient ─── */ - -.glass-border-gradient { - position: relative; -} - -.glass-border-gradient::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - padding: 1px; - background: linear-gradient(135deg, var(--glass-border-hover), transparent 60%); - mask: - linear-gradient(#fff 0 0) content-box, - linear-gradient(#fff 0 0); - mask-composite: exclude; - -webkit-mask-composite: xor; - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; -} - -.glass-border-gradient:hover::before { - opacity: 1; -} - -/* ─── Ambient Background ─── */ - -.ambient-bg { - position: relative; -} - -.ambient-bg::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: -1; - background: - radial-gradient(ellipse 80% 50% at 20% 80%, var(--kn-claw-dim) 0%, transparent 60%), - radial-gradient(ellipse 60% 40% at 80% 20%, var(--kn-ocean-dim) 0%, transparent 50%); -} - -.ambient-bg::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: -1; - background: - radial-gradient(ellipse 50% 30% at 60% 60%, var(--kn-claw-dim) 0%, transparent 50%), - radial-gradient(ellipse 40% 50% at 30% 30%, rgba(0, 212, 170, 0.03) 0%, transparent 50%); - animation: ambient-drift 120s ease-in-out infinite alternate; - background-size: 200% 200%; -} - -/* ─── Typography Utilities ─── */ - -.text-display { - font-weight: 700; - letter-spacing: -0.04em; - line-height: 1.1; -} - -/* ─── Glass Dashboard Card ─── */ - -.glass-dashboard-card { - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - padding: 1.25rem; - overflow: hidden; - position: relative; - box-shadow: var(--shadow-sm), var(--glass-highlight); - transition: - border-color 0.2s ease, - box-shadow 0.2s ease; - min-width: 0; -} - -.glass-dashboard-card::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, transparent, var(--accent), transparent); - opacity: 0; - transition: opacity 0.2s ease; -} - -.glass-dashboard-card:hover { - border-color: var(--glass-border-hover); - box-shadow: var(--shadow-md); -} - -.glass-dashboard-card:hover::after { - opacity: 0.6; -} - -/* ─── Card Header Convention ─── */ - -.card-header { - display: flex; - align-items: center; - gap: 0.625rem; - margin-bottom: 0.875rem; - min-height: 28px; -} - -.card-header__prefix { - color: var(--accent); - font-family: var(--mono); - font-size: 0.82rem; - font-weight: 600; - line-height: 1; -} - -.card-header__title { - font-size: 0.9rem; - font-weight: 700; - color: var(--text); - letter-spacing: -0.01em; - margin: 0; -} - -.card-header__actions { - margin-left: auto; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.card-header__link { - font-size: 0.75rem; - color: var(--accent); - text-decoration: none; - display: inline-flex; - align-items: center; - gap: 4px; - cursor: pointer; - white-space: nowrap; -} - -.card-header__link:hover { - text-decoration: underline; -} - -/* ─── Count Badge ─── */ - -.count-badge { - font-size: 0.72rem; - font-family: var(--mono); - font-variant-numeric: tabular-nums; - background: var(--clay-bg-card); - color: var(--muted); - padding: 1px 7px; - border-radius: 9999px; - line-height: 1.4; - white-space: nowrap; -} - -.count-badge--accent { - color: var(--accent); -} - -.count-badge--emerald { - color: var(--success); -} - -.count-badge--amber { - color: var(--warn); -} - -.count-badge--red { - color: var(--danger); -} - -/* ─── Glass Divider ─── */ - -.glass-divider { - height: 1px; - background: var(--clay-border-subtle); - margin: 1.25rem 0; - border: none; -} - -/* ─── Glass Event Row ─── */ - -.glass-event-row { - padding: 6px 8px; - border-radius: var(--clay-radius-sm); - cursor: pointer; - transition: background var(--clay-duration-fast) ease; -} - -.glass-event-row:hover { - background: var(--clay-bg-interactive); -} diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 384d89c9399..b939c27c29d 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -5,8 +5,8 @@ .shell { --shell-pad: 16px; --shell-gap: 16px; - --shell-nav-width: 240px; - --shell-topbar-height: 62px; + --shell-nav-width: 220px; + --shell-topbar-height: 56px; --shell-focus-duration: 200ms; --shell-focus-ease: var(--ease-out); height: 100vh; @@ -14,7 +14,7 @@ grid-template-columns: var(--shell-nav-width) minmax(0, 1fr); grid-template-rows: var(--shell-topbar-height) 1fr; grid-template-areas: - "nav topbar" + "topbar topbar" "nav content"; gap: 0; animation: dashboard-enter 0.4s var(--ease-out); @@ -41,7 +41,7 @@ } .shell--nav-collapsed { - grid-template-columns: 60px minmax(0, 1fr); + grid-template-columns: 0px minmax(0, 1fr); } .shell--chat-focus { @@ -80,262 +80,139 @@ display: flex; justify-content: space-between; align-items: center; - gap: 12px; + gap: 16px; padding: 0 20px; height: var(--shell-topbar-height); - background: var(--topbar-bg); - backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur)); - -webkit-backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur)); - border-bottom: var(--topbar-border); + border-bottom: 1px solid var(--border); + background: var(--bg); } -/* --- Left: Dashboard Header --- */ - -.dashboard-header { +.topbar-left { display: flex; align-items: center; - gap: 0.5rem; - min-width: 0; + gap: 12px; } -.dashboard-header__breadcrumb { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.82rem; - min-width: 0; +.topbar .nav-collapse-toggle { + width: 36px; + height: 36px; + margin-bottom: 0; } -.dashboard-header__breadcrumb-link { - color: var(--muted); - text-decoration: none; - cursor: pointer; - white-space: nowrap; -} - -.dashboard-header__breadcrumb-link:hover { - color: var(--text); -} - -.dashboard-header__breadcrumb-sep { - color: var(--muted); - opacity: 0.5; -} - -.dashboard-header__breadcrumb-current { - color: var(--text); - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dashboard-header__actions { - margin-left: auto; - display: flex; - align-items: center; - gap: 8px; -} - -/* --- Center: Search / Command Palette Trigger --- */ - -.topbar-search { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 12px; - min-width: 200px; - max-width: 340px; - flex: 1; - height: 34px; - border: 1px solid var(--border); - border-radius: var(--radius-full); - background: color-mix(in srgb, var(--secondary) 60%, transparent); - color: var(--muted); - font-size: 13px; - font-family: var(--font-body); - cursor: pointer; - transition: - border-color 180ms ease, - background 180ms ease, - box-shadow 180ms ease; - -webkit-appearance: none; - appearance: none; -} - -.topbar-search:hover { - border-color: var(--border-strong); - background: color-mix(in srgb, var(--secondary) 85%, transparent); -} - -.topbar-search:focus-visible { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.topbar-search__label { - flex: 1; - text-align: left; - pointer-events: none; -} - -.topbar-search__kbd { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 1px 6px; - min-width: 22px; +.topbar .nav-collapse-toggle__icon { + width: 20px; height: 20px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--bg) 70%, transparent); - color: var(--muted); - font-size: 11px; - font-family: var(--font-body); - font-weight: 500; - line-height: 1; - pointer-events: none; - flex-shrink: 0; } -/* --- Right: Status area --- */ +.topbar .nav-collapse-toggle__icon svg { + width: 20px; + height: 20px; +} -.topbar-status { +/* Brand */ +.brand { display: flex; align-items: center; gap: 10px; +} + +.brand-logo { + width: 28px; + height: 28px; flex-shrink: 0; } -.topbar-divider { - width: 1px; - height: 20px; - background: var(--border); - flex-shrink: 0; +.brand-logo img { + width: 100%; + height: 100%; + object-fit: contain; } -/* Connection indicator */ +.brand-text { + display: flex; + flex-direction: column; + gap: 1px; +} -.topbar-connection { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - border-radius: var(--radius-full); - font-size: 12px; +.brand-title { + font-size: 16px; + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1.1; + color: var(--text-strong); +} + +.brand-sub { + font-size: 10px; font-weight: 500; - color: var(--danger); - background: var(--danger-subtle); - transition: - color 250ms ease, - background 250ms ease; -} - -.topbar-connection--ok { - color: var(--ok); - background: var(--ok-subtle); -} - -.topbar-connection__dot { - width: 6px; - height: 6px; - border-radius: var(--radius-full); - background: currentColor; - box-shadow: 0 0 6px currentColor; - flex-shrink: 0; -} - -.topbar-connection:not(.topbar-connection--ok) .topbar-connection__dot { - animation: pulse-subtle 2s ease-in-out infinite; -} - -.topbar-connection__label { + color: var(--muted); + letter-spacing: 0.05em; text-transform: uppercase; - letter-spacing: 0.04em; line-height: 1; } -/* Redact / stream-mode toggle */ - -.topbar-redact { - display: inline-flex; +/* Topbar status */ +.topbar-status { + display: flex; align-items: center; - justify-content: center; - width: 28px; - height: 28px; - padding: 0; - border: 1px solid transparent; - border-radius: var(--radius); - background: none; - color: var(--muted); - cursor: pointer; - transition: - color 180ms ease, - background 180ms ease, - border-color 180ms ease; - flex-shrink: 0; + gap: 8px; } -.topbar-redact svg { - width: 14px; - height: 14px; +.topbar-status .pill { + padding: 6px 10px; + gap: 6px; + font-size: 12px; + font-weight: 500; + height: 32px; + box-sizing: border-box; } -.topbar-redact:hover { - color: var(--text); - background: color-mix(in srgb, var(--secondary) 80%, transparent); - border-color: var(--border); +.topbar-status .pill .mono { + display: flex; + align-items: center; + line-height: 1; + margin-top: 0px; } -.topbar-redact--active { - color: var(--warn); +.topbar-status .statusDot { + width: 6px; + height: 6px; } -.topbar-redact--active:hover { - color: var(--warn); - background: var(--warn-subtle); - border-color: color-mix(in srgb, var(--warn) 30%, transparent); -} - -/* Topbar theme toggle sizing */ - .topbar-status .theme-toggle { - height: 30px; + --theme-item: 24px; + --theme-gap: 2px; + --theme-pad: 3px; } -.topbar-status .theme-btn svg { - width: 13px; - height: 13px; +.topbar-status .theme-icon { + width: 12px; + height: 12px; } /* =========================================== Navigation Sidebar =========================================== */ -.sidebar { +.nav { grid-area: nav; - display: flex; - flex-direction: column; overflow-y: auto; overflow-x: hidden; - scrollbar-width: none; - background: var(--sidebar-bg); - backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur)); - -webkit-backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur)); + padding: 16px 12px; + background: var(--bg); + scrollbar-width: none; /* Firefox */ transition: width var(--shell-focus-duration) var(--shell-focus-ease), padding var(--shell-focus-duration) var(--shell-focus-ease), opacity var(--shell-focus-duration) var(--shell-focus-ease); min-height: 0; - border-right: 1px solid var(--glass-border); } -.sidebar::-webkit-scrollbar { - display: none; +.nav::-webkit-scrollbar { + display: none; /* Chrome/Safari */ } -.shell--chat-focus .sidebar { +.shell--chat-focus .nav { width: 0; padding: 0; border-width: 0; @@ -344,141 +221,51 @@ opacity: 0; } -.sidebar--collapsed { - align-items: center; -} - -.sidebar--collapsed .sidebar-header { - justify-content: center; - padding: 10px 8px; - min-height: 54px; -} - -.sidebar--collapsed .nav-group__items { - padding: 4px 0; - align-items: center; -} - -.sidebar--collapsed .nav-item { - margin: 0; - padding: 10px; - justify-content: center; - width: 44px; - height: 44px; -} - -.sidebar--collapsed .nav-item__icon { - width: 22px; - height: 22px; - opacity: 0.85; -} - -.sidebar--collapsed .nav-item__icon svg { - width: 22px; - height: 22px; - stroke-width: 1.75px; -} - -.sidebar--collapsed .nav-item--active { - border-left: 0; -} - -.sidebar--collapsed .sidebar-footer { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 0; -} - -.sidebar--collapsed .sidebar-footer .nav-item { - margin: 0; - padding: 10px; - width: 44px; - height: 44px; -} - -/* Sidebar header (brand + collapse) */ -.sidebar-header { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px 8px; - gap: 0; - flex-shrink: 0; - min-height: 54px; -} - -.sidebar-brand { - flex: 2; - display: flex; - align-items: center; - gap: 10px; +.nav--collapsed { + width: 0; min-width: 0; - - max-height: 28px; - - padding-left: 10px; - padding-right: 10px; - - @media (max-width: 1100px) { - padding-left: 0; - padding-right: 0; - } + padding: 0; + overflow: hidden; + border: none; + opacity: 0; + pointer-events: none; } -.sidebar-brand__logo { - width: 28px; - height: 28px; - flex-shrink: 0; - object-fit: contain; -} - -.sidebar-brand__title { - font-size: 15px; - font-weight: 700; - letter-spacing: -0.03em; - line-height: 1.1; - color: var(--text-strong); - white-space: nowrap; -} - -.sidebar-collapse-btn { - flex: 1; +/* Nav collapse toggle */ +.nav-collapse-toggle { + width: 32px; height: 32px; - - @media (max-width: 1100px) { - height: 28px; - } - display: flex; align-items: center; justify-content: center; - background: var(--bg); - border: var(--border) 1px solid transparent; - border-radius: var(--radius-sm); + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius-md); cursor: pointer; - color: var(--muted); - flex-shrink: 0; transition: background var(--duration-fast) ease, - border-color var(--duration-fast) ease, - color var(--duration-fast) ease; + border-color var(--duration-fast) ease; + margin-bottom: 16px; } -.sidebar--collapsed .sidebar-collapse-btn { - flex: none; - width: 100%; -} - -.sidebar-collapse-btn:hover { - background: var(--bg); +.nav-collapse-toggle:hover { + background: var(--bg-hover); border-color: var(--border); - color: var(--text); } -.sidebar-collapse-btn svg { - width: 24px; - height: 24px; +.nav-collapse-toggle__icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + color: var(--muted); + transition: color var(--duration-fast) ease; +} + +.nav-collapse-toggle__icon svg { + width: 18px; + height: 18px; stroke: currentColor; fill: none; stroke-width: 1.5px; @@ -486,22 +273,13 @@ stroke-linejoin: round; } -/* Sidebar nav section */ -.sidebar-nav { - flex: 1; - padding: 4px 8px; - overflow-y: auto; - overflow-x: hidden; - scrollbar-width: none; -} - -.sidebar-nav::-webkit-scrollbar { - display: none; +.nav-collapse-toggle:hover .nav-collapse-toggle__icon { + color: var(--text); } /* Nav groups */ .nav-group { - margin-bottom: 16px; + margin-bottom: 20px; display: grid; gap: 2px; } @@ -519,16 +297,16 @@ display: none; } -/* Nav group label */ -.nav-group__label { +/* Nav label */ +.nav-label { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; padding: 6px 10px; - font-size: 12px; - font-weight: 600; + font-size: 11px; + font-weight: 500; color: var(--muted); margin-bottom: 4px; background: transparent; @@ -536,40 +314,37 @@ cursor: pointer; text-align: left; border-radius: var(--radius-sm); - text-transform: uppercase; - letter-spacing: 0.04em; transition: color var(--duration-fast) ease, background var(--duration-fast) ease; } -.nav-group__label:hover { +.nav-label:hover { color: var(--text); background: var(--bg-hover); } -.nav-group__label-text { +.nav-label--static { + cursor: default; +} + +.nav-label--static:hover { + color: var(--muted); + background: transparent; +} + +.nav-label__text { flex: 1; } -.nav-group__chevron { - display: flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; +.nav-label__chevron { + font-size: 10px; opacity: 0.5; transition: transform var(--duration-fast) ease; } -.nav-group__chevron svg { - width: 12px; - height: 12px; - stroke: currentColor; - fill: none; - stroke-width: 2px; - stroke-linecap: round; - stroke-linejoin: round; +.nav-group--collapsed .nav-label__chevron { + transform: rotate(-90deg); } /* Nav items */ @@ -579,7 +354,7 @@ align-items: center; justify-content: flex-start; gap: 10px; - padding: 9px 12px; + padding: 8px 10px; border-radius: var(--radius-md); border: 1px solid transparent; background: transparent; @@ -589,13 +364,12 @@ transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease, - color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; + color var(--duration-fast) ease; } .nav-item__icon { - width: 18px; - height: 18px; + width: 16px; + height: 16px; display: flex; align-items: center; justify-content: center; @@ -605,8 +379,8 @@ } .nav-item__icon svg { - width: 18px; - height: 18px; + width: 16px; + height: 16px; stroke: currentColor; fill: none; stroke-width: 1.5px; @@ -615,32 +389,14 @@ } .nav-item__text { - font-size: 14px; + font-size: 13px; font-weight: 500; white-space: nowrap; } -.nav-item__external-icon { - display: flex; - align-items: center; - margin-left: auto; - opacity: 0.4; -} - -.nav-item__external-icon svg { - width: 12px; - height: 12px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - .nav-item:hover { color: var(--text); - background: color-mix(in srgb, var(--secondary) 90%, transparent); - border-color: color-mix(in srgb, var(--border) 75%, transparent); + background: var(--bg-hover); text-decoration: none; } @@ -648,55 +404,23 @@ opacity: 1; } -.nav-item--active { +.nav-item.active { color: var(--text-strong); - background: color-mix(in srgb, var(--accent-subtle) 70%, var(--secondary)); - border-color: color-mix(in srgb, var(--accent) 34%, transparent); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent); + background: var(--accent-subtle); } -.nav-item--active .nav-item__icon { +.nav-item.active .nav-item__icon { opacity: 1; color: var(--accent); } -/* Sidebar footer — aligned with chat compose bar */ -.sidebar-footer { - padding: 14px 8px 6px; - border-top: 1px solid var(--border); - flex-shrink: 0; - margin-top: auto; -} - -.sidebar-version { - display: flex; - align-items: center; - justify-content: center; - padding: 6px 10px; -} - -.sidebar-version__text { - font-size: 12px; - font-weight: 500; - color: var(--muted); - letter-spacing: 0.02em; -} - -.sidebar-version__dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--muted); - opacity: 0.4; -} - /* =========================================== Content Area =========================================== */ .content { grid-area: content; - padding: 14px 18px 36px; + padding: 12px 16px 32px; display: block; min-height: 0; overflow-y: auto; @@ -707,6 +431,10 @@ margin-top: 24px; } +:root[data-theme="light"] .content { + background: var(--bg-content); +} + .content--chat { display: flex; flex-direction: column; @@ -725,7 +453,7 @@ align-items: flex-end; justify-content: space-between; gap: 16px; - padding: 4px 0; + padding: 4px 8px; overflow: hidden; transform-origin: top center; transition: @@ -745,7 +473,7 @@ } .page-title { - font-size: 28px; + font-size: 26px; font-weight: 700; letter-spacing: -0.035em; line-height: 1.15; @@ -754,7 +482,7 @@ .page-sub { color: var(--muted); - font-size: 15px; + font-size: 14px; font-weight: 400; margin-top: 6px; letter-spacing: -0.01em; @@ -849,31 +577,16 @@ "content"; } - .sidebar { + .nav { position: static; max-height: none; display: flex; - flex-direction: row; gap: 6px; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--border); - } - - .sidebar-header { - display: none; - } - - .sidebar-footer { - display: none; - } - - .sidebar-nav { - display: flex; - flex-direction: row; - gap: 6px; padding: 10px 14px; - overflow-x: auto; + background: var(--bg); } .nav-group { @@ -893,12 +606,8 @@ gap: 10px; } - .topbar-search__kbd { - display: none; - } - .topbar-status { - flex-wrap: nowrap; + flex-wrap: wrap; } .table-head, diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css index 084373ab82f..450a83608c6 100644 --- a/ui/src/styles/layout.mobile.css +++ b/ui/src/styles/layout.mobile.css @@ -4,22 +4,7 @@ /* Tablet: Horizontal nav */ @media (max-width: 1100px) { - .sidebar { - flex-direction: row; - flex-wrap: nowrap; - border-right: none; - border-bottom: 1px solid var(--border); - } - - .sidebar-header { - display: none; - } - - .sidebar-footer { - display: none; - } - - .sidebar-nav { + .nav { display: flex; flex-direction: row; flex-wrap: nowrap; @@ -30,7 +15,7 @@ scrollbar-width: none; } - .sidebar-nav::-webkit-scrollbar { + .nav::-webkit-scrollbar { display: none; } @@ -42,7 +27,7 @@ display: contents; } - .nav-group__label { + .nav-label { display: none; } @@ -71,56 +56,53 @@ padding: 10px 12px; gap: 8px; flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; justify-content: space-between; align-items: center; } - .sidebar-brand__title { + .brand { + flex: 1; + min-width: 0; + } + + .brand-title { font-size: 14px; } - .dashboard-header__breadcrumb-link, - .dashboard-header__breadcrumb-sep { - display: none; - } - - .topbar-search { - min-width: 0; - max-width: none; - flex: 1; - } - - .topbar-search__label { - display: none; - } - - .topbar-search__kbd { - display: none; - } - - .topbar-connection__label { - display: none; - } - - .topbar-divider { + .brand-sub { display: none; } .topbar-status { gap: 6px; + width: auto; flex-wrap: nowrap; } + .topbar-status .pill { + padding: 4px 8px; + font-size: 11px; + gap: 4px; + } + + .topbar-status .pill .mono { + display: none; + } + + .topbar-status .pill span:nth-child(2) { + display: none; + } + /* Nav */ - .sidebar-nav { + .nav { padding: 8px 10px; gap: 4px; -webkit-overflow-scrolling: touch; scrollbar-width: none; } - .sidebar-nav::-webkit-scrollbar { + .nav::-webkit-scrollbar { display: none; } @@ -128,7 +110,7 @@ display: contents; } - .nav-group__label { + .nav-label { display: none; } @@ -306,13 +288,11 @@ font-size: 11px; } + /* Theme toggle */ .theme-toggle { - height: 28px; - } - - .theme-btn svg { - width: 12px; - height: 12px; + --theme-item: 24px; + --theme-gap: 2px; + --theme-pad: 3px; } .theme-icon { @@ -331,11 +311,11 @@ padding: 8px 10px; } - .sidebar-brand__title { + .brand-title { font-size: 13px; } - .sidebar-nav { + .nav { padding: 6px 8px; } @@ -376,12 +356,15 @@ font-size: 11px; } - .topbar-connection { + .topbar-status .pill { padding: 3px 6px; + font-size: 10px; } .theme-toggle { - height: 26px; + --theme-item: 22px; + --theme-gap: 2px; + --theme-pad: 2px; } .theme-icon { diff --git a/ui/src/ui/app-gateway.node.test.ts b/ui/src/ui/app-gateway.node.test.ts index c0b9b8b0403..30e4a1203ca 100644 --- a/ui/src/ui/app-gateway.node.test.ts +++ b/ui/src/ui/app-gateway.node.test.ts @@ -50,7 +50,7 @@ function createHost() { token: "", sessionKey: "main", lastActiveSessionKey: "main", - theme: "dark", + theme: "system", chatFocusMode: false, chatShowThinking: true, splitRatio: 0.6, diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 4aacd29c51f..4126b5707c3 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -24,7 +24,6 @@ import { parseExecApprovalResolved, removeExecApproval, } from "./controllers/exec-approval.ts"; -import { loadHealthState } from "./controllers/health.ts"; import { loadNodes } from "./controllers/nodes.ts"; import { loadSessions } from "./controllers/sessions.ts"; import type { GatewayEventFrame, GatewayHelloOk } from "./gateway.ts"; @@ -34,7 +33,7 @@ import type { UiSettings } from "./storage.ts"; import type { AgentsListResult, PresenceEntry, - HealthSummary, + HealthSnapshot, StatusSummary, UpdateAvailable, } from "./types.ts"; @@ -56,10 +55,7 @@ type GatewayHost = { agentsLoading: boolean; agentsList: AgentsListResult | null; agentsError: string | null; - healthLoading: boolean; - healthResult: HealthSummary | null; - healthError: string | null; - debugHealth: HealthSummary | null; + debugHealth: HealthSnapshot | null; assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; @@ -160,7 +156,6 @@ export function connectGateway(host: GatewayHost) { resetToolStream(host as unknown as Parameters[0]); void loadAssistantIdentity(host as unknown as OpenClawApp); void loadAgents(host as unknown as OpenClawApp); - void loadHealthState(host as unknown as OpenClawApp); void loadNodes(host as unknown as OpenClawApp, { quiet: true }); void loadDevices(host as unknown as OpenClawApp, { quiet: true }); void refreshActiveTab(host as unknown as Parameters[0]); @@ -206,7 +201,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { { ts: Date.now(), event: evt.event, payload: evt.payload }, ...host.eventLogBuffer, ].slice(0, 250); - if (host.tab === "debug" || host.tab === "overview") { + if (host.tab === "debug") { host.eventLog = host.eventLogBuffer; } @@ -298,7 +293,7 @@ export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) { const snapshot = hello.snapshot as | { presence?: PresenceEntry[]; - health?: HealthSummary; + health?: HealthSnapshot; sessionDefaults?: SessionDefaultsSnapshot; updateAvailable?: UpdateAvailable; } @@ -308,7 +303,6 @@ export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) { } if (snapshot?.health) { host.debugHealth = snapshot.health; - host.healthResult = snapshot.health; } if (snapshot?.sessionDefaults) { applySessionDefaults(host, snapshot.sessionDefaults); diff --git a/ui/src/ui/app-lifecycle.ts b/ui/src/ui/app-lifecycle.ts index f7d8d5c1ef2..41442714108 100644 --- a/ui/src/ui/app-lifecycle.ts +++ b/ui/src/ui/app-lifecycle.ts @@ -10,6 +10,8 @@ import { import { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from "./app-scroll.ts"; import { applySettingsFromUrl, + attachThemeListener, + detachThemeListener, inferBasePath, syncTabWithLocation, syncThemeWithSettings, @@ -36,28 +38,14 @@ type LifecycleHost = { topbarObserver: ResizeObserver | null; }; -function handleCmdK(host: LifecycleHost, e: KeyboardEvent) { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { - e.preventDefault(); - (host as unknown as { paletteOpen: boolean }).paletteOpen = !( - host as unknown as { paletteOpen: boolean } - ).paletteOpen; - } -} - export function handleConnected(host: LifecycleHost) { host.basePath = inferBasePath(); void loadControlUiBootstrapConfig(host); applySettingsFromUrl(host as unknown as Parameters[0]); syncTabWithLocation(host as unknown as Parameters[0], true); syncThemeWithSettings(host as unknown as Parameters[0]); + attachThemeListener(host as unknown as Parameters[0]); window.addEventListener("popstate", host.popStateHandler); - (host as unknown as { cmdKHandler: (e: KeyboardEvent) => void }).cmdKHandler = (e) => - handleCmdK(host, e); - window.addEventListener( - "keydown", - (host as unknown as { cmdKHandler: (e: KeyboardEvent) => void }).cmdKHandler, - ); connectGateway(host as unknown as Parameters[0]); startNodesPolling(host as unknown as Parameters[0]); if (host.tab === "logs") { @@ -74,13 +62,10 @@ export function handleFirstUpdated(host: LifecycleHost) { export function handleDisconnected(host: LifecycleHost) { window.removeEventListener("popstate", host.popStateHandler); - const cmdK = (host as unknown as { cmdKHandler?: (e: KeyboardEvent) => void }).cmdKHandler; - if (cmdK) { - window.removeEventListener("keydown", cmdK); - } stopNodesPolling(host as unknown as Parameters[0]); stopLogsPolling(host as unknown as Parameters[0]); stopDebugPolling(host as unknown as Parameters[0]); + detachThemeListener(host as unknown as Parameters[0]); host.topbarObserver?.disconnect(); host.topbarObserver = null; } diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index d7610962872..d954147297b 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -1,4 +1,4 @@ -import { html, nothing } from "lit"; +import { html } from "lit"; import { repeat } from "lit/directives/repeat.js"; import { t } from "../i18n/index.ts"; import { refreshChat } from "./app-chat.ts"; @@ -49,12 +49,10 @@ function resetChatStateForSessionSwitch(state: AppViewState, sessionKey: string) export function renderTab(state: AppViewState, tab: Tab) { const href = pathForTab(tab, state.basePath); - const isActive = state.tab === tab; - const collapsed = state.settings.navCollapsed; return html` { if ( event.defaultPrevented || @@ -79,7 +77,7 @@ export function renderTab(state: AppViewState, tab: Tab) { title=${titleForTab(tab)} > - ${!collapsed ? html`${titleForTab(tab)}` : nothing} + ${titleForTab(tab)} `; } @@ -396,18 +394,10 @@ function resolveSessionOptions( return options; } -type ThemeOption = { id: ThemeMode; label: string; iconKey: keyof typeof icons }; -const THEME_OPTIONS: ThemeOption[] = [ - { id: "dark", label: "Dark", iconKey: "monitor" }, - { id: "light", label: "Light", iconKey: "book" }, - { id: "openknot", label: "Knot", iconKey: "zap" }, - { id: "fieldmanual", label: "Field", iconKey: "terminal" }, - { id: "openai", label: "Ember", iconKey: "loader" }, - { id: "clawdash", label: "Chrome", iconKey: "settings" }, -]; +const THEME_ORDER: ThemeMode[] = ["system", "light", "dark"]; export function renderThemeToggle(state: AppViewState) { - const app = state as unknown as OpenClawApp; + const index = Math.max(0, THEME_ORDER.indexOf(state.theme)); const applyTheme = (next: ThemeMode) => (event: MouseEvent) => { const element = event.currentTarget as HTMLElement; const context: ThemeTransitionContext = { element }; @@ -418,34 +408,74 @@ export function renderThemeToggle(state: AppViewState) { state.setTheme(next, context); }; - const handleCollapse = () => app.handleThemeToggleCollapse(); - return html` -
{ - const toggle = e.currentTarget as HTMLElement; - requestAnimationFrame(() => { - if (!toggle.contains(document.activeElement)) { - handleCollapse(); - } - }); - }} - > - ${state.themeOrder.map((id) => { - const opt = THEME_OPTIONS.find((o) => o.id === id)!; - return html` - - `; - })} +
+
+ + + + +
`; } + +function renderSunIcon() { + return html` + + `; +} + +function renderMoonIcon() { + return html` + + `; +} + +function renderMonitorIcon() { + return html` + + `; +} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index b56dea7a89b..a87f9a8059c 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -1,8 +1,5 @@ import { html, nothing } from "lit"; -import { - buildAgentMainSessionKey, - parseAgentSessionKey, -} from "../../../src/routing/session-key.js"; +import { parseAgentSessionKey } from "../../../src/routing/session-key.js"; import { t } from "../i18n/index.ts"; import { refreshChatAvatar } from "./app-chat.ts"; import { renderUsageTab } from "./app-render-usage-tab.ts"; @@ -55,21 +52,17 @@ import { updateSkillEdit, updateSkillEnabled, } from "./controllers/skills.ts"; -import "./components/dashboard-header.ts"; import { icons } from "./icons.ts"; import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts"; import { renderAgents } from "./views/agents.ts"; -import { renderBottomTabs } from "./views/bottom-tabs.ts"; import { renderChannels } from "./views/channels.ts"; import { renderChat } from "./views/chat.ts"; -import { renderCommandPalette } from "./views/command-palette.ts"; import { renderConfig } from "./views/config.ts"; import { renderCron } from "./views/cron.ts"; import { renderDebug } from "./views/debug.ts"; import { renderExecApprovalPrompt } from "./views/exec-approval.ts"; import { renderGatewayUrlConfirmation } from "./views/gateway-url-confirmation.ts"; import { renderInstances } from "./views/instances.ts"; -import { renderLoginGate } from "./views/login-gate.ts"; import { renderLogs } from "./views/logs.ts"; import { renderNodes } from "./views/nodes.ts"; import { renderOverview } from "./views/overview.ts"; @@ -96,15 +89,6 @@ function resolveAssistantAvatarUrl(state: AppViewState): string | undefined { } export function renderApp(state: AppViewState) { - // Gate: require successful gateway connection before showing the dashboard. - // The gateway URL confirmation overlay is always rendered so URL-param flows still work. - if (!state.connected) { - return html` - ${renderLoginGate(state)} - ${renderGatewayUrlConfirmation(state)} - `; - } - const presenceCount = state.presenceEntries.length; const sessionsCount = state.sessionsResult?.count ?? null; const cronNext = state.cronStatus?.nextWakeAtMs ?? null; @@ -124,165 +108,83 @@ export function renderApp(state: AppViewState) { null; return html` - ${renderCommandPalette({ - open: state.paletteOpen, - query: (state as unknown as { paletteQuery?: string }).paletteQuery ?? "", - activeIndex: (state as unknown as { paletteActiveIndex?: number }).paletteActiveIndex ?? 0, - onToggle: () => { - state.paletteOpen = !state.paletteOpen; - }, - onQueryChange: (q) => { - (state as unknown as { paletteQuery: string }).paletteQuery = q; - }, - onActiveIndexChange: (i) => { - (state as unknown as { paletteActiveIndex: number }).paletteActiveIndex = i; - }, - onNavigate: (tab) => { - state.setTab(tab as import("./navigation.ts").Tab); - }, - onSlashCommand: (_cmd) => { - state.setTab("chat" as import("./navigation.ts").Tab); - }, - })}
- - -
+
- -
- - ${state.connected ? t("common.ok") : t("common.offline")} +
+ +
+
OPENCLAW
+
Gateway Dashboard
+
+
+
+
+
+ + ${t("common.health")} + ${state.connected ? t("common.ok") : t("common.offline")}
- ${renderThemeToggle(state)}
-
@@ -323,15 +225,6 @@ export function renderApp(state: AppViewState) { cronEnabled: state.cronStatus?.enabled ?? null, cronNext, lastChannelsRefresh: state.channelsLastSuccess, - usageResult: state.usageResult, - sessionsResult: state.sessionsResult, - skillsReport: state.skillsReport, - cronJobs: state.cronJobs, - cronStatus: state.cronStatus, - attentionItems: state.attentionItems, - eventLog: state.eventLog, - overviewLogLines: state.overviewLogLines, - streamMode: state.streamMode, onSettingsChange: (next) => state.applySettings(next), onPasswordChange: (next) => (state.password = next), onSessionKeyChange: (next) => { @@ -347,16 +240,6 @@ export function renderApp(state: AppViewState) { }, onConnect: () => state.connect(), onRefresh: () => state.loadOverview(), - onNavigate: (tab) => state.setTab(tab as import("./navigation.ts").Tab), - onRefreshLogs: () => state.loadOverview(), - onToggleStreamMode: () => { - state.streamMode = !state.streamMode; - try { - localStorage.setItem("openclaw:stream-mode", String(state.streamMode)); - } catch { - /* */ - } - }, }) : nothing } @@ -407,7 +290,6 @@ export function renderApp(state: AppViewState) { entries: state.presenceEntries, lastError: state.presenceError, statusMessage: state.presenceStatus, - streamMode: state.streamMode, onRefresh: () => loadPresence(state), }) : nothing @@ -476,47 +358,33 @@ export function renderApp(state: AppViewState) { agentsList: state.agentsList, selectedAgentId: resolvedAgentId, activePanel: state.agentsPanel, - config: { - form: configValue, - loading: state.configLoading, - saving: state.configSaving, - dirty: state.configFormDirty, - }, - channels: { - snapshot: state.channelsSnapshot, - loading: state.channelsLoading, - error: state.channelsError, - lastSuccess: state.channelsLastSuccess, - }, - cron: { - status: state.cronStatus, - jobs: state.cronJobs, - loading: state.cronLoading, - error: state.cronError, - }, - agentFiles: { - list: state.agentFilesList, - loading: state.agentFilesLoading, - error: state.agentFilesError, - active: state.agentFileActive, - contents: state.agentFileContents, - drafts: state.agentFileDrafts, - saving: state.agentFileSaving, - }, + configForm: configValue, + configLoading: state.configLoading, + configSaving: state.configSaving, + configDirty: state.configFormDirty, + channelsLoading: state.channelsLoading, + channelsError: state.channelsError, + channelsSnapshot: state.channelsSnapshot, + channelsLastSuccess: state.channelsLastSuccess, + cronLoading: state.cronLoading, + cronStatus: state.cronStatus, + cronJobs: state.cronJobs, + cronError: state.cronError, + agentFilesLoading: state.agentFilesLoading, + agentFilesError: state.agentFilesError, + agentFilesList: state.agentFilesList, + agentFileActive: state.agentFileActive, + agentFileContents: state.agentFileContents, + agentFileDrafts: state.agentFileDrafts, + agentFileSaving: state.agentFileSaving, agentIdentityLoading: state.agentIdentityLoading, agentIdentityError: state.agentIdentityError, agentIdentityById: state.agentIdentityById, - agentSkills: { - report: state.agentSkillsReport, - loading: state.agentSkillsLoading, - error: state.agentSkillsError, - agentId: state.agentSkillsAgentId, - filter: state.skillsFilter, - }, - sidebarFilter: state.agentsSidebarFilter, - onSidebarFilterChange: (value) => { - state.agentsSidebarFilter = value; - }, + agentSkillsLoading: state.agentSkillsLoading, + agentSkillsReport: state.agentSkillsReport, + agentSkillsError: state.agentSkillsError, + agentSkillsAgentId: state.agentSkillsAgentId, + skillsFilter: state.skillsFilter, onRefresh: async () => { await loadAgents(state); const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? []; @@ -655,9 +523,6 @@ export function renderApp(state: AppViewState) { onConfigSave: () => saveConfig(state), onChannelsRefresh: () => loadChannels(state, false), onCronRefresh: () => state.loadCron(), - onCronRunNow: (_jobId) => { - // Stub: backend support pending - }, onSkillsFilterChange: (next) => (state.skillsFilter = next), onSkillsRefresh: () => { if (resolvedAgentId) { @@ -827,12 +692,6 @@ export function renderApp(state: AppViewState) { : { fallbacks: normalized }; updateConfigFormValue(state, basePath, next); }, - onSetDefault: (agentId) => { - if (!configValue) { - return; - } - updateConfigFormValue(state, ["agents", "defaultId"], agentId); - }, }) : nothing } @@ -1001,45 +860,6 @@ export function renderApp(state: AppViewState) { onAbort: () => void state.handleAbortChat(), onQueueRemove: (id) => state.removeQueuedMessage(id), onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }), - onClearHistory: async () => { - if (!state.client || !state.connected) { - return; - } - try { - await state.client.request("sessions.reset", { key: state.sessionKey }); - state.chatMessages = []; - state.chatStream = null; - state.chatRunId = null; - await loadChatHistory(state); - } catch (err) { - state.lastError = String(err); - } - }, - agentsList: state.agentsList, - currentAgentId: resolvedAgentId ?? "main", - onAgentChange: (agentId: string) => { - state.sessionKey = buildAgentMainSessionKey({ agentId }); - state.chatMessages = []; - state.chatStream = null; - state.chatRunId = null; - state.applySettings({ - ...state.settings, - sessionKey: state.sessionKey, - lastActiveSessionKey: state.sessionKey, - }); - void loadChatHistory(state); - void state.loadAssistantIdentity(); - }, - onNavigateToAgent: () => { - state.agentsSelectedId = resolvedAgentId; - state.setTab("agents" as import("./navigation.ts").Tab); - }, - onSessionSelect: (key: string) => { - state.setSessionKey(key); - state.chatMessages = []; - void loadChatHistory(state); - void state.loadAssistantIdentity(); - }, showNewMessages: state.chatNewMessagesBelow && !state.chatManualRefreshInFlight, onScrollToBottom: () => state.scrollToBottom(), // Sidebar props for tool output viewing @@ -1077,7 +897,6 @@ export function renderApp(state: AppViewState) { searchQuery: state.configSearchQuery, activeSection: state.configActiveSection, activeSubsection: state.configActiveSubsection, - streamMode: state.streamMode, onRawChange: (next) => { state.configRaw = next; }, @@ -1143,10 +962,6 @@ export function renderApp(state: AppViewState) {
${renderExecApprovalPrompt(state)} ${renderGatewayUrlConfirmation(state)} - ${renderBottomTabs({ - activeTab: state.tab, - onTabChange: (tab) => state.setTab(tab), - })}
`; } diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts index e1b05791306..48411bbe5b0 100644 --- a/ui/src/ui/app-settings.test.ts +++ b/ui/src/ui/app-settings.test.ts @@ -13,14 +13,14 @@ const createHost = (tab: Tab): SettingsHost => ({ token: "", sessionKey: "main", lastActiveSessionKey: "main", - theme: "dark", + theme: "system", chatFocusMode: false, chatShowThinking: true, splitRatio: 0.6, navCollapsed: false, navGroupsCollapsed: {}, }, - theme: "dark", + theme: "system", themeResolved: "dark", applySessionKey: "main", sessionKey: "main", @@ -31,6 +31,8 @@ const createHost = (tab: Tab): SettingsHost => ({ eventLog: [], eventLogBuffer: [], basePath: "", + themeMedia: null, + themeMediaHandler: null, logsPollInterval: null, debugPollInterval: null, }); diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 1d50cd9852c..7415e468e0b 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -21,7 +21,6 @@ import { loadNodes } from "./controllers/nodes.ts"; import { loadPresence } from "./controllers/presence.ts"; import { loadSessions } from "./controllers/sessions.ts"; import { loadSkills } from "./controllers/skills.ts"; -import { loadUsage } from "./controllers/usage.ts"; import { inferBasePathFromPathname, normalizeBasePath, @@ -33,7 +32,7 @@ import { import { saveSettings, type UiSettings } from "./storage.ts"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition.ts"; import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme.ts"; -import type { AgentsListResult, AttentionItem } from "./types.ts"; +import type { AgentsListResult } from "./types.ts"; type SettingsHost = { settings: UiSettings; @@ -52,6 +51,8 @@ type SettingsHost = { agentsList?: AgentsListResult | null; agentsSelectedId?: string | null; agentsPanel?: "overview" | "files" | "tools" | "skills" | "channels" | "cron"; + themeMedia: MediaQueryList | null; + themeMediaHandler: ((event: MediaQueryListEvent) => void) | null; pendingGatewayUrl?: string | null; }; @@ -258,7 +259,7 @@ export function inferBasePath() { } export function syncThemeWithSettings(host: SettingsHost) { - host.theme = host.settings.theme ?? "dark"; + host.theme = host.settings.theme ?? "system"; applyResolvedTheme(host, resolveTheme(host.theme)); } @@ -269,7 +270,44 @@ export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) } const root = document.documentElement; root.dataset.theme = resolved; - root.style.colorScheme = "dark"; + root.style.colorScheme = resolved; +} + +export function attachThemeListener(host: SettingsHost) { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") { + return; + } + host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); + host.themeMediaHandler = (event) => { + if (host.theme !== "system") { + return; + } + applyResolvedTheme(host, event.matches ? "dark" : "light"); + }; + if (typeof host.themeMedia.addEventListener === "function") { + host.themeMedia.addEventListener("change", host.themeMediaHandler); + return; + } + const legacy = host.themeMedia as MediaQueryList & { + addListener: (cb: (event: MediaQueryListEvent) => void) => void; + }; + legacy.addListener(host.themeMediaHandler); +} + +export function detachThemeListener(host: SettingsHost) { + if (!host.themeMedia || !host.themeMediaHandler) { + return; + } + if (typeof host.themeMedia.removeEventListener === "function") { + host.themeMedia.removeEventListener("change", host.themeMediaHandler); + return; + } + const legacy = host.themeMedia as MediaQueryList & { + removeListener: (cb: (event: MediaQueryListEvent) => void) => void; + }; + legacy.removeListener(host.themeMediaHandler); + host.themeMedia = null; + host.themeMediaHandler = null; } export function syncTabWithLocation(host: SettingsHost, replace: boolean) { @@ -365,121 +403,13 @@ export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, re } export async function loadOverview(host: SettingsHost) { - const app = host as unknown as OpenClawApp; - await Promise.allSettled([ - loadChannels(app, false), - loadPresence(app), - loadSessions(app), - loadCronStatus(app), - loadCronJobs(app), - loadDebug(app), - loadSkills(app), - loadUsage(app), - loadOverviewLogs(app), + await Promise.all([ + loadChannels(host as unknown as OpenClawApp, false), + loadPresence(host as unknown as OpenClawApp), + loadSessions(host as unknown as OpenClawApp), + loadCronStatus(host as unknown as OpenClawApp), + loadDebug(host as unknown as OpenClawApp), ]); - buildAttentionItems(app); -} - -async function loadOverviewLogs(host: OpenClawApp) { - if (!host.client || !host.connected) { - return; - } - try { - const res = await host.client.request("logs.tail", { - cursor: host.overviewLogCursor || undefined, - limit: 100, - maxBytes: 50_000, - }); - const payload = res as { - cursor?: number; - lines?: unknown; - }; - const lines = Array.isArray(payload.lines) - ? payload.lines.filter((line): line is string => typeof line === "string") - : []; - host.overviewLogLines = [...host.overviewLogLines, ...lines].slice(-500); - if (typeof payload.cursor === "number") { - host.overviewLogCursor = payload.cursor; - } - } catch { - /* non-critical */ - } -} - -function buildAttentionItems(host: OpenClawApp) { - const items: AttentionItem[] = []; - - if (host.lastError) { - items.push({ - severity: "error", - icon: "x", - title: "Gateway Error", - description: host.lastError, - }); - } - - const hello = host.hello; - const auth = (hello as { auth?: { scopes?: string[] } } | null)?.auth; - if (auth?.scopes && !auth.scopes.includes("operator.read")) { - items.push({ - severity: "warning", - icon: "key", - title: "Missing operator.read scope", - description: - "This connection does not have the operator.read scope. Some features may be unavailable.", - href: "https://docs.openclaw.ai/web/dashboard", - external: true, - }); - } - - const skills = host.skillsReport?.skills ?? []; - const missingDeps = skills.filter((s) => !s.disabled && Object.keys(s.missing).length > 0); - if (missingDeps.length > 0) { - const names = missingDeps.slice(0, 3).map((s) => s.name); - const more = missingDeps.length > 3 ? ` +${missingDeps.length - 3} more` : ""; - items.push({ - severity: "warning", - icon: "zap", - title: "Skills with missing dependencies", - description: `${names.join(", ")}${more}`, - }); - } - - const blocked = skills.filter((s) => s.blockedByAllowlist); - if (blocked.length > 0) { - items.push({ - severity: "warning", - icon: "shield", - title: `${blocked.length} skill${blocked.length > 1 ? "s" : ""} blocked`, - description: blocked.map((s) => s.name).join(", "), - }); - } - - const cronJobs = host.cronJobs ?? []; - const failedCron = cronJobs.filter((j) => j.state?.lastStatus === "error"); - if (failedCron.length > 0) { - items.push({ - severity: "error", - icon: "clock", - title: `${failedCron.length} cron job${failedCron.length > 1 ? "s" : ""} failed`, - description: failedCron.map((j) => j.name).join(", "), - }); - } - - const now = Date.now(); - const overdue = cronJobs.filter( - (j) => j.enabled && j.state?.nextRunAtMs != null && now - j.state.nextRunAtMs > 300_000, - ); - if (overdue.length > 0) { - items.push({ - severity: "warning", - icon: "clock", - title: `${overdue.length} overdue job${overdue.length > 1 ? "s" : ""}`, - description: overdue.map((j) => j.name).join(", "), - }); - } - - host.attentionItems = items; } export async function loadChannelsTab(host: SettingsHost) { diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 5ee23477ba6..e7c7735c8bf 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -8,22 +8,20 @@ import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts"; import type { Tab } from "./navigation.ts"; import type { UiSettings } from "./storage.ts"; import type { ThemeTransitionContext } from "./theme-transition.ts"; -import type { ResolvedTheme, ThemeMode } from "./theme.ts"; +import type { ThemeMode } from "./theme.ts"; import type { AgentsListResult, AgentsFilesListResult, AgentIdentityResult, - AttentionItem, ChannelsStatusSnapshot, ConfigSnapshot, ConfigUiHints, CronJob, CronRunLogEntry, CronStatus, - HealthSummary, + HealthSnapshot, LogEntry, LogLevel, - ModelCatalogEntry, NostrProfile, PresenceEntry, SessionsUsageResult, @@ -45,8 +43,7 @@ export type AppViewState = { basePath: string; connected: boolean; theme: ThemeMode; - themeResolved: ResolvedTheme; - themeOrder: ThemeMode[]; + themeResolved: "light" | "dark"; hello: GatewayHelloOk | null; lastError: string | null; eventLog: EventLogEntry[]; @@ -146,7 +143,6 @@ export type AppViewState = { agentSkillsError: string | null; agentSkillsReport: SkillStatusReport | null; agentSkillsAgentId: string | null; - agentsSidebarFilter: string; sessionsLoading: boolean; sessionsResult: SessionsListResult | null; sessionsError: string | null; @@ -204,13 +200,10 @@ export type AppViewState = { skillEdits: Record; skillMessages: Record; skillsBusyKey: string | null; - healthLoading: boolean; - healthResult: HealthSummary | null; - healthError: string | null; debugLoading: boolean; debugStatus: StatusSummary | null; - debugHealth: HealthSummary | null; - debugModels: ModelCatalogEntry[]; + debugHealth: HealthSnapshot | null; + debugModels: unknown[]; debugHeartbeat: unknown; debugCallMethod: string; debugCallParams: string; @@ -230,12 +223,6 @@ export type AppViewState = { logsMaxBytes: number; logsAtBottom: boolean; updateAvailable: import("./types.js").UpdateAvailable | null; - // Overview dashboard state - attentionItems: AttentionItem[]; - paletteOpen: boolean; - streamMode: boolean; - overviewLogLines: string[]; - overviewLogCursor: number; client: GatewayBrowserClient | null; refreshSessionsAfterChat: Set; connect: () => void; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 1c284079c93..db4b290b10e 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -60,7 +60,7 @@ import type { SkillMessage } from "./controllers/skills.ts"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts"; import type { Tab } from "./navigation.ts"; import { loadSettings, type UiSettings } from "./storage.ts"; -import { VALID_THEMES, type ResolvedTheme, type ThemeMode } from "./theme.ts"; +import type { ResolvedTheme, ThemeMode } from "./theme.ts"; import type { AgentsListResult, AgentsFilesListResult, @@ -70,10 +70,9 @@ import type { CronJob, CronRunLogEntry, CronStatus, - HealthSummary, + HealthSnapshot, LogEntry, LogLevel, - ModelCatalogEntry, PresenceEntry, ChannelsStatusSnapshot, SessionsListResult, @@ -119,9 +118,8 @@ export class OpenClawApp extends LitElement { @state() tab: Tab = "chat"; @state() onboarding = resolveOnboardingMode(); @state() connected = false; - @state() theme: ThemeMode = this.settings.theme ?? "dark"; + @state() theme: ThemeMode = this.settings.theme ?? "system"; @state() themeResolved: ResolvedTheme = "dark"; - @state() themeOrder: ThemeMode[] = this.buildThemeOrder(this.theme); @state() hello: GatewayHelloOk | null = null; @state() lastError: string | null = null; @state() eventLog: EventLogEntry[] = []; @@ -231,7 +229,6 @@ export class OpenClawApp extends LitElement { @state() agentSkillsError: string | null = null; @state() agentSkillsReport: SkillStatusReport | null = null; @state() agentSkillsAgentId: string | null = null; - @state() agentsSidebarFilter = ""; @state() sessionsLoading = false; @state() sessionsResult: SessionsListResult | null = null; @@ -307,23 +304,6 @@ export class OpenClawApp extends LitElement { @state() updateAvailable: import("./types.js").UpdateAvailable | null = null; - // Overview dashboard state - @state() attentionItems: import("./types.js").AttentionItem[] = []; - @state() paletteOpen = false; - paletteQuery = ""; - paletteActiveIndex = 0; - @state() streamMode = (() => { - try { - const stored = localStorage.getItem("openclaw:stream-mode"); - // Default to true (redacted) unless explicitly disabled - return stored === null ? true : stored === "true"; - } catch { - return true; - } - })(); - @state() overviewLogLines: string[] = []; - @state() overviewLogCursor = 0; - @state() skillsLoading = false; @state() skillsReport: SkillStatusReport | null = null; @state() skillsError: string | null = null; @@ -332,14 +312,10 @@ export class OpenClawApp extends LitElement { @state() skillsBusyKey: string | null = null; @state() skillMessages: Record = {}; - @state() healthLoading = false; - @state() healthResult: HealthSummary | null = null; - @state() healthError: string | null = null; - @state() debugLoading = false; @state() debugStatus: StatusSummary | null = null; - @state() debugHealth: HealthSummary | null = null; - @state() debugModels: ModelCatalogEntry[] = []; + @state() debugHealth: HealthSnapshot | null = null; + @state() debugModels: unknown[] = []; @state() debugHeartbeat: unknown = null; @state() debugCallMethod = ""; @state() debugCallParams = "{}"; @@ -378,6 +354,8 @@ export class OpenClawApp extends LitElement { basePath = ""; private popStateHandler = () => onPopStateInternal(this as unknown as Parameters[0]); + private themeMedia: MediaQueryList | null = null; + private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null; private topbarObserver: ResizeObserver | null = null; createRenderRoot() { @@ -455,19 +433,6 @@ export class OpenClawApp extends LitElement { setTheme(next: ThemeMode, context?: Parameters[2]) { setThemeInternal(this as unknown as Parameters[0], next, context); - this.themeOrder = this.buildThemeOrder(next); - } - - buildThemeOrder(active: ThemeMode): ThemeMode[] { - const all = [...VALID_THEMES]; - const rest = all.filter((id) => id !== active); - return [active, ...rest]; - } - - handleThemeToggleCollapse() { - setTimeout(() => { - this.themeOrder = this.buildThemeOrder(this.theme); - }, 80); } async loadOverview() { diff --git a/ui/src/ui/chat/deleted-messages.ts b/ui/src/ui/chat/deleted-messages.ts deleted file mode 100644 index fd3916d78c7..00000000000 --- a/ui/src/ui/chat/deleted-messages.ts +++ /dev/null @@ -1,49 +0,0 @@ -const PREFIX = "openclaw:deleted:"; - -export class DeletedMessages { - private key: string; - private _keys = new Set(); - - constructor(sessionKey: string) { - this.key = PREFIX + sessionKey; - this.load(); - } - - has(key: string): boolean { - return this._keys.has(key); - } - - delete(key: string): void { - this._keys.add(key); - this.save(); - } - - restore(key: string): void { - this._keys.delete(key); - this.save(); - } - - clear(): void { - this._keys.clear(); - this.save(); - } - - private load(): void { - try { - const raw = localStorage.getItem(this.key); - if (!raw) { - return; - } - const arr = JSON.parse(raw); - if (Array.isArray(arr)) { - this._keys = new Set(arr.filter((s) => typeof s === "string")); - } - } catch { - // ignore - } - } - - private save(): void { - localStorage.setItem(this.key, JSON.stringify([...this._keys])); - } -} diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 0eb3f2251f8..7c36713c3c0 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -1,10 +1,9 @@ import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import type { AssistantIdentity } from "../assistant-identity.ts"; -import { icons } from "../icons.ts"; import { toSanitizedMarkdownHtml } from "../markdown.ts"; import { detectTextDirection } from "../text-direction.ts"; -import type { MessageGroup, ToolCard } from "../types/chat-types.ts"; +import type { MessageGroup } from "../types/chat-types.ts"; import { renderCopyAsMarkdownButton } from "./copy-as-markdown.ts"; import { extractTextCached, @@ -112,7 +111,6 @@ export function renderMessageGroup( showReasoning: boolean; assistantName?: string; assistantAvatar?: string | null; - onDelete?: () => void; }, ) { const normalizedRole = normalizeRoleForGrouping(group.role); @@ -150,16 +148,6 @@ export function renderMessageGroup(
@@ -228,66 +216,6 @@ function renderMessageImages(images: ImageBlock[]) { `; } -/** Render tool cards inside a collapsed `
` element. */ -function renderCollapsedToolCards( - toolCards: ToolCard[], - onOpenSidebar?: (content: string) => void, -) { - const calls = toolCards.filter((c) => c.kind === "call"); - const results = toolCards.filter((c) => c.kind === "result"); - const totalTools = Math.max(calls.length, results.length) || toolCards.length; - const toolNames = [...new Set(toolCards.map((c) => c.name))]; - const summaryLabel = - toolNames.length <= 3 - ? toolNames.join(", ") - : `${toolNames.slice(0, 2).join(", ")} +${toolNames.length - 2} more`; - - return html` -
- - ${icons.zap} - ${totalTools} tool${totalTools === 1 ? "" : "s"} - ${summaryLabel} - -
- ${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))} -
-
- `; -} - -/** - * Detect whether a trimmed string is a JSON object or array. - * Must start with `{`/`[` and end with `}`/`]` and parse successfully. - */ -function detectJson(text: string): { parsed: unknown; pretty: string } | null { - const t = text.trim(); - if ((t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]"))) { - try { - const parsed = JSON.parse(t); - return { parsed, pretty: JSON.stringify(parsed, null, 2) }; - } catch { - return null; - } - } - return null; -} - -/** Build a short summary label for collapsed JSON (type + key count or array length). */ -function jsonSummaryLabel(parsed: unknown): string { - if (Array.isArray(parsed)) { - return `Array (${parsed.length} item${parsed.length === 1 ? "" : "s"})`; - } - if (parsed && typeof parsed === "object") { - const keys = Object.keys(parsed as Record); - if (keys.length <= 4) { - return `{ ${keys.join(", ")} }`; - } - return `Object (${keys.length} keys)`; - } - return "JSON"; -} - function renderGroupedMessage( message: unknown, opts: { isStreaming: boolean; showReasoning: boolean }, @@ -315,9 +243,6 @@ function renderGroupedMessage( const markdown = markdownBase; const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim()); - // Detect pure-JSON messages and render as collapsible block - const jsonResult = markdown && !opts.isStreaming ? detectJson(markdown) : null; - const bubbleClasses = [ "chat-bubble", canCopyMarkdown ? "has-copy" : "", @@ -328,7 +253,7 @@ function renderGroupedMessage( .join(" "); if (!markdown && hasToolCards && isToolResult) { - return renderCollapsedToolCards(toolCards, onOpenSidebar); + return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`; } if (!markdown && !hasToolCards && !hasImages) { @@ -347,19 +272,11 @@ function renderGroupedMessage( : nothing } ${ - jsonResult - ? html`
- - JSON - ${jsonSummaryLabel(jsonResult.parsed)} - -
${jsonResult.pretty}
-
` - : markdown - ? html`
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
` - : nothing + markdown + ? html`
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
` + : nothing } - ${hasToolCards ? renderCollapsedToolCards(toolCards, onOpenSidebar) : nothing} + ${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))} `; } diff --git a/ui/src/ui/chat/input-history.ts b/ui/src/ui/chat/input-history.ts deleted file mode 100644 index 34d8806d072..00000000000 --- a/ui/src/ui/chat/input-history.ts +++ /dev/null @@ -1,49 +0,0 @@ -const MAX = 50; - -export class InputHistory { - private items: string[] = []; - private cursor = -1; - - push(text: string): void { - const trimmed = text.trim(); - if (!trimmed) { - return; - } - if (this.items[this.items.length - 1] === trimmed) { - return; - } - this.items.push(trimmed); - if (this.items.length > MAX) { - this.items.shift(); - } - this.cursor = -1; - } - - up(): string | null { - if (this.items.length === 0) { - return null; - } - if (this.cursor < 0) { - this.cursor = this.items.length - 1; - } else if (this.cursor > 0) { - this.cursor--; - } - return this.items[this.cursor] ?? null; - } - - down(): string | null { - if (this.cursor < 0) { - return null; - } - this.cursor++; - if (this.cursor >= this.items.length) { - this.cursor = -1; - return null; - } - return this.items[this.cursor] ?? null; - } - - reset(): void { - this.cursor = -1; - } -} diff --git a/ui/src/ui/chat/pinned-messages.ts b/ui/src/ui/chat/pinned-messages.ts deleted file mode 100644 index 4914b0db32a..00000000000 --- a/ui/src/ui/chat/pinned-messages.ts +++ /dev/null @@ -1,61 +0,0 @@ -const PREFIX = "openclaw:pinned:"; - -export class PinnedMessages { - private key: string; - private _indices = new Set(); - - constructor(sessionKey: string) { - this.key = PREFIX + sessionKey; - this.load(); - } - - get indices(): Set { - return this._indices; - } - - has(index: number): boolean { - return this._indices.has(index); - } - - pin(index: number): void { - this._indices.add(index); - this.save(); - } - - unpin(index: number): void { - this._indices.delete(index); - this.save(); - } - - toggle(index: number): void { - if (this._indices.has(index)) { - this.unpin(index); - } else { - this.pin(index); - } - } - - clear(): void { - this._indices.clear(); - this.save(); - } - - private load(): void { - try { - const raw = localStorage.getItem(this.key); - if (!raw) { - return; - } - const arr = JSON.parse(raw); - if (Array.isArray(arr)) { - this._indices = new Set(arr.filter((n) => typeof n === "number")); - } - } catch { - // ignore - } - } - - private save(): void { - localStorage.setItem(this.key, JSON.stringify([...this._indices])); - } -} diff --git a/ui/src/ui/chat/slash-commands.ts b/ui/src/ui/chat/slash-commands.ts deleted file mode 100644 index 48e6c838817..00000000000 --- a/ui/src/ui/chat/slash-commands.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { IconName } from "../icons.ts"; - -export type SlashCommandCategory = "session" | "model" | "agents" | "tools"; - -export type SlashCommandDef = { - name: string; - description: string; - args?: string; - icon?: IconName; - category?: SlashCommandCategory; -}; - -export const SLASH_COMMANDS: SlashCommandDef[] = [ - { name: "help", description: "Show available commands", icon: "book", category: "session" }, - { name: "status", description: "Show current status", icon: "barChart", category: "session" }, - { name: "reset", description: "Reset session", icon: "refresh", category: "session" }, - { name: "compact", description: "Compact session context", icon: "loader", category: "session" }, - { name: "stop", description: "Stop current run", icon: "stop", category: "session" }, - { - name: "model", - description: "Show/set model", - args: "", - icon: "brain", - category: "model", - }, - { - name: "think", - description: "Set thinking level", - args: "", - icon: "brain", - category: "model", - }, - { - name: "verbose", - description: "Toggle verbose mode", - args: "", - icon: "terminal", - category: "model", - }, - { name: "export", description: "Export session to HTML", icon: "download", category: "tools" }, - { - name: "skill", - description: "Run a skill", - args: "", - icon: "zap", - category: "tools", - }, - { name: "agents", description: "List agents", icon: "monitor", category: "agents" }, - { - name: "kill", - description: "Abort sub-agents", - args: "", - icon: "x", - category: "agents", - }, - { - name: "steer", - description: "Steer a sub-agent", - args: " ", - icon: "send", - category: "agents", - }, - { name: "usage", description: "Show token usage", icon: "barChart", category: "tools" }, -]; - -const CATEGORY_ORDER: SlashCommandCategory[] = ["session", "model", "agents", "tools"]; - -export const CATEGORY_LABELS: Record = { - session: "Session", - model: "Model", - agents: "Agents", - tools: "Tools", -}; - -export function getSlashCommandCompletions(filter: string): SlashCommandDef[] { - const commands = filter - ? SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(filter.toLowerCase())) - : SLASH_COMMANDS; - return commands.toSorted((a, b) => { - const ai = CATEGORY_ORDER.indexOf(a.category ?? "session"); - const bi = CATEGORY_ORDER.indexOf(b.category ?? "session"); - return ai - bi; - }); -} diff --git a/ui/src/ui/components/dashboard-header.ts b/ui/src/ui/components/dashboard-header.ts deleted file mode 100644 index cf5f9795c0b..00000000000 --- a/ui/src/ui/components/dashboard-header.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { LitElement, html } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { titleForTab, type Tab } from "../navigation.js"; - -@customElement("dashboard-header") -export class DashboardHeader extends LitElement { - override createRenderRoot() { - return this; - } - - @property() tab: Tab = "overview"; - - override render() { - const label = titleForTab(this.tab); - - return html` -
-
- this.dispatchEvent(new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }))} - > - ClawDash - - - ${label} -
-
- -
-
- `; - } -} diff --git a/ui/src/ui/config-form.browser.test.ts b/ui/src/ui/config-form.browser.test.ts index b391a27f928..292c5780b35 100644 --- a/ui/src/ui/config-form.browser.test.ts +++ b/ui/src/ui/config-form.browser.test.ts @@ -197,7 +197,7 @@ describe("config form renderer", () => { expect(container.textContent).toContain("Plugin Enabled"); }); - it("passes mixed unions through for JSON fallback rendering", () => { + it("flags unsupported unions", () => { const schema = { type: "object", properties: { @@ -207,7 +207,7 @@ describe("config form renderer", () => { }, }; const analysis = analyzeConfigSchema(schema); - expect(analysis.unsupportedPaths).not.toContain("mixed"); + expect(analysis.unsupportedPaths).toContain("mixed"); }); it("supports nullable types", () => { diff --git a/ui/src/ui/controllers/debug.ts b/ui/src/ui/controllers/debug.ts index 3fb743c56a0..b4dfa7ade4d 100644 --- a/ui/src/ui/controllers/debug.ts +++ b/ui/src/ui/controllers/debug.ts @@ -1,24 +1,18 @@ import type { GatewayBrowserClient } from "../gateway.ts"; -import type { HealthSummary, ModelCatalogEntry, StatusSummary } from "../types.ts"; -import { loadHealthState } from "./health.ts"; -import { loadModels } from "./models.ts"; +import type { HealthSnapshot, StatusSummary } from "../types.ts"; export type DebugState = { client: GatewayBrowserClient | null; connected: boolean; debugLoading: boolean; debugStatus: StatusSummary | null; - debugHealth: HealthSummary | null; - debugModels: ModelCatalogEntry[]; + debugHealth: HealthSnapshot | null; + debugModels: unknown[]; debugHeartbeat: unknown; debugCallMethod: string; debugCallParams: string; debugCallResult: string | null; debugCallError: string | null; - /** Shared health state fields (written by {@link loadHealthState}). */ - healthLoading: boolean; - healthResult: HealthSummary | null; - healthError: string | null; }; export async function loadDebug(state: DebugState) { @@ -30,16 +24,16 @@ export async function loadDebug(state: DebugState) { } state.debugLoading = true; try { - const [status, , models, heartbeat] = await Promise.all([ + const [status, health, models, heartbeat] = await Promise.all([ state.client.request("status", {}), - loadHealthState(state), - loadModels(state.client), + state.client.request("health", {}), + state.client.request("models.list", {}), state.client.request("last-heartbeat", {}), ]); state.debugStatus = status as StatusSummary; - // Sync debugHealth from the shared healthResult for backward compat. - state.debugHealth = state.healthResult; - state.debugModels = models; + state.debugHealth = health as HealthSnapshot; + const modelPayload = models as { models?: unknown[] } | undefined; + state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : []; state.debugHeartbeat = heartbeat; } catch (err) { state.debugCallError = String(err); diff --git a/ui/src/ui/controllers/health.ts b/ui/src/ui/controllers/health.ts deleted file mode 100644 index b077794d67a..00000000000 --- a/ui/src/ui/controllers/health.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { GatewayBrowserClient } from "../gateway.ts"; -import type { HealthSummary } from "../types.ts"; - -/** Default fallback returned when the gateway is unreachable or returns null. */ -const HEALTH_FALLBACK: HealthSummary = { - ok: false, - ts: 0, - durationMs: 0, - heartbeatSeconds: 0, - defaultAgentId: "", - agents: [], - sessions: { path: "", count: 0, recent: [] }, -}; - -/** State slice consumed by {@link loadHealthState}. Follows the agents/sessions convention. */ -export type HealthState = { - client: GatewayBrowserClient | null; - connected: boolean; - healthLoading: boolean; - healthResult: HealthSummary | null; - healthError: string | null; -}; - -/** - * Fetch the gateway health summary. - * - * Accepts a {@link GatewayBrowserClient} (matching the existing ui/ controller - * convention). Returns a fully-typed {@link HealthSummary}; on failure the - * caller receives a safe fallback with `ok: false` rather than `null`. - */ -export async function loadHealth(client: GatewayBrowserClient): Promise { - try { - const result = await client.request("health", {}); - return result ?? HEALTH_FALLBACK; - } catch { - return HEALTH_FALLBACK; - } -} - -/** - * State-mutating health loader (same pattern as {@link import("./agents.ts").loadAgents}). - * - * Populates `healthResult` / `healthError` on the provided state slice and - * toggles `healthLoading` around the request. - */ -export async function loadHealthState(state: HealthState): Promise { - if (!state.client || !state.connected) { - return; - } - if (state.healthLoading) { - return; - } - state.healthLoading = true; - state.healthError = null; - try { - state.healthResult = await loadHealth(state.client); - } catch (err) { - state.healthError = String(err); - } finally { - state.healthLoading = false; - } -} diff --git a/ui/src/ui/controllers/models.ts b/ui/src/ui/controllers/models.ts deleted file mode 100644 index d9e119c5c3a..00000000000 --- a/ui/src/ui/controllers/models.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { GatewayBrowserClient } from "../gateway.ts"; -import type { ModelCatalogEntry } from "../types.ts"; - -/** - * Fetch the model catalog from the gateway. - * - * Accepts a {@link GatewayBrowserClient} (matching the existing ui/ controller - * convention). Returns an array of {@link ModelCatalogEntry}; on failure the - * caller receives an empty array rather than throwing. - */ -export async function loadModels(client: GatewayBrowserClient): Promise { - try { - const result = await client.request<{ models: ModelCatalogEntry[] }>("models.list", {}); - return result?.models ?? []; - } catch { - return []; - } -} diff --git a/ui/src/ui/format.ts b/ui/src/ui/format.ts index e0c92baba3d..da3d544f199 100644 --- a/ui/src/ui/format.ts +++ b/ui/src/ui/format.ts @@ -58,41 +58,3 @@ export function parseList(input: string): string[] { export function stripThinkingTags(value: string): string { return stripReasoningTagsFromText(value, { mode: "preserve", trim: "start" }); } - -export function formatCost(cost: number | null | undefined, fallback = "$0.00"): string { - if (cost == null || !Number.isFinite(cost)) { - return fallback; - } - if (cost === 0) { - return "$0.00"; - } - if (cost < 0.01) { - return `$${cost.toFixed(4)}`; - } - if (cost < 1) { - return `$${cost.toFixed(3)}`; - } - return `$${cost.toFixed(2)}`; -} - -export function formatTokens(tokens: number | null | undefined, fallback = "0"): string { - if (tokens == null || !Number.isFinite(tokens)) { - return fallback; - } - if (tokens < 1000) { - return String(Math.round(tokens)); - } - if (tokens < 1_000_000) { - const k = tokens / 1000; - return k < 10 ? `${k.toFixed(1)}k` : `${Math.round(k)}k`; - } - const m = tokens / 1_000_000; - return m < 10 ? `${m.toFixed(1)}M` : `${Math.round(m)}M`; -} - -export function formatPercent(value: number | null | undefined, fallback = "—"): string { - if (value == null || !Number.isFinite(value)) { - return fallback; - } - return `${(value * 100).toFixed(1)}%`; -} diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 39ef7ec1c8e..975cca4ab5a 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -61,13 +61,6 @@ export type GatewayBrowserClientOptions = { // 4008 = application-defined code (browser rejects 1008 "Policy Violation") const CONNECT_FAILED_CLOSE_CODE = 4008; -const DEFAULT_OPERATOR_CONNECT_SCOPES = [ - "operator.admin", - "operator.read", - "operator.write", - "operator.approvals", - "operator.pairing", -]; export class GatewayBrowserClient { private ws: WebSocket | null = null; @@ -136,11 +129,6 @@ export class GatewayBrowserClient { if (this.connectSent) { return; } - const nonce = this.connectNonce?.trim() ?? ""; - if (!nonce) { - this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect challenge missing nonce"); - return; - } this.connectSent = true; if (this.connectTimer !== null) { window.clearTimeout(this.connectTimer); @@ -152,9 +140,10 @@ export class GatewayBrowserClient { // Gateways may reject this unless gateway.controlUi.allowInsecureAuth is enabled. const isSecureContext = typeof crypto !== "undefined" && !!crypto.subtle; - const scopes = DEFAULT_OPERATOR_CONNECT_SCOPES; + const scopes = ["operator.admin", "operator.approvals", "operator.pairing"]; const role = "operator"; let deviceIdentity: Awaited> | null = null; + let canFallbackToShared = false; let authToken = this.opts.token; if (isSecureContext) { @@ -164,6 +153,7 @@ export class GatewayBrowserClient { role, })?.token; authToken = storedToken ?? this.opts.token; + canFallbackToShared = Boolean(storedToken && this.opts.token); } const auth = authToken || this.opts.password @@ -179,12 +169,13 @@ export class GatewayBrowserClient { publicKey: string; signature: string; signedAt: number; - nonce: string; + nonce: string | undefined; } | undefined; if (isSecureContext && deviceIdentity) { const signedAtMs = Date.now(); + const nonce = this.connectNonce ?? undefined; const payload = buildDeviceAuthPayload({ deviceId: deviceIdentity.deviceId, clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.CONTROL_UI, @@ -237,11 +228,7 @@ export class GatewayBrowserClient { this.opts.onHello?.(hello); }) .catch(() => { - // Clear stale device token on any connect failure so the next attempt - // falls back to the shared gateway token (if present) or retries without - // a cached device token. Without this, a rotated/revoked device token - // causes an infinite mismatch loop when no shared token is configured. - if (deviceIdentity) { + if (canFallbackToShared && deviceIdentity) { clearDeviceAuthToken({ deviceId: deviceIdentity.deviceId, role }); } this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect failed"); @@ -262,12 +249,10 @@ export class GatewayBrowserClient { if (evt.event === "connect.challenge") { const payload = evt.payload as { nonce?: unknown } | undefined; const nonce = payload && typeof payload.nonce === "string" ? payload.nonce : null; - if (!nonce || nonce.trim().length === 0) { - this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect challenge missing nonce"); - return; + if (nonce) { + this.connectNonce = nonce; + void this.sendConnect(); } - this.connectNonce = nonce.trim(); - void this.sendConnect(); return; } const seq = typeof evt.seq === "number" ? evt.seq : null; @@ -321,10 +306,7 @@ export class GatewayBrowserClient { window.clearTimeout(this.connectTimer); } this.connectTimer = window.setTimeout(() => { - if (this.connectSent || this.ws?.readyState !== WebSocket.OPEN) { - return; - } - this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect challenge timeout"); - }, 2_000); + void this.sendConnect(); + }, 750); } } diff --git a/ui/src/ui/icons.ts b/ui/src/ui/icons.ts index 5a42ef89130..1682dcfa9d3 100644 --- a/ui/src/ui/icons.ts +++ b/ui/src/ui/icons.ts @@ -228,147 +228,6 @@ export const icons = { /> `, - panelLeftClose: html` - - - - - - `, - panelLeftOpen: html` - - - - - - `, - chevronDown: html` - - - - `, - chevronRight: html` - - - - `, - externalLink: html` - - - - - `, - send: html` - - - - - `, - stop: html` - - `, - pin: html` - - - - - `, - pinOff: html` - - - - - - `, - download: html` - - - - - - `, - mic: html` - - - - - - `, - micOff: html` - - - - - - - - - `, - bookmark: html` - - `, - plus: html` - - - - - `, - terminal: html` - - - - - `, - spark: html` - - - - `, - refresh: html` - - - - - `, - trash: html` - - - - - - - - `, - eye: html` - - - - - `, - eyeOff: html` - - - - - - - `, } as const; export type IconName = keyof typeof icons; diff --git a/ui/src/ui/markdown.ts b/ui/src/ui/markdown.ts index e892402e5d6..1867b0eda46 100644 --- a/ui/src/ui/markdown.ts +++ b/ui/src/ui/markdown.ts @@ -14,7 +14,6 @@ const allowedTags = [ "br", "code", "del", - "details", "em", "h1", "h2", @@ -27,7 +26,6 @@ const allowedTags = [ "p", "pre", "strong", - "summary", "table", "tbody", "td", @@ -134,35 +132,6 @@ export function toSanitizedMarkdownHtml(markdown: string): string { const htmlEscapeRenderer = new marked.Renderer(); htmlEscapeRenderer.html = ({ text }: { text: string }) => escapeHtml(text); -htmlEscapeRenderer.code = ({ - text, - lang, - escaped, -}: { - text: string; - lang?: string; - escaped: boolean; -}) => { - const langClass = lang ? ` class="language-${lang}"` : ""; - const safeText = escaped ? text : escapeHtml(text); - const codeBlock = `
${safeText}
`; - - const trimmed = text.trim(); - const isJson = - lang === "json" || - (!lang && - ((trimmed.startsWith("{") && trimmed.endsWith("}")) || - (trimmed.startsWith("[") && trimmed.endsWith("]")))); - - if (isJson) { - const lineCount = text.split("\n").length; - const label = lineCount > 1 ? `JSON · ${lineCount} lines` : "JSON"; - return `
${label}${codeBlock}
`; - } - - return codeBlock; -}; - function escapeHtml(value: string): string { return value .replace(/&/g, "&") diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index e9803088576..b32e6c3c5b2 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -1,7 +1,7 @@ const KEY = "openclaw.control.settings.v1"; import { isSupportedLocale } from "../i18n/index.ts"; -import { VALID_THEMES, type ThemeMode } from "./theme.ts"; +import type { ThemeMode } from "./theme.ts"; export type UiSettings = { gatewayUrl: string; @@ -28,7 +28,7 @@ export function loadSettings(): UiSettings { token: "", sessionKey: "main", lastActiveSessionKey: "main", - theme: "dark", + theme: "system", chatFocusMode: false, chatShowThinking: true, splitRatio: 0.6, @@ -57,9 +57,10 @@ export function loadSettings(): UiSettings { ? parsed.lastActiveSessionKey.trim() : (typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()) || defaults.lastActiveSessionKey, - theme: VALID_THEMES.has(parsed.theme as ThemeMode) - ? (parsed.theme as ThemeMode) - : defaults.theme, + theme: + parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system" + ? parsed.theme + : defaults.theme, chatFocusMode: typeof parsed.chatFocusMode === "boolean" ? parsed.chatFocusMode : defaults.chatFocusMode, chatShowThinking: diff --git a/ui/src/ui/theme.ts b/ui/src/ui/theme.ts index c27f8b280d2..480f9dbe51a 100644 --- a/ui/src/ui/theme.ts +++ b/ui/src/ui/theme.ts @@ -1,26 +1,16 @@ -export type ThemeMode = "dark" | "light" | "openknot" | "fieldmanual" | "openai" | "clawdash"; -export type ResolvedTheme = ThemeMode; +export type ThemeMode = "system" | "light" | "dark"; +export type ResolvedTheme = "light" | "dark"; -export const VALID_THEMES = new Set([ - "dark", - "light", - "openknot", - "fieldmanual", - "openai", - "clawdash", -]); - -const LEGACY_MAP: Record = { - defaultTheme: "dark", - docsTheme: "light", - lightTheme: "openknot", - landingTheme: "openknot", - newTheme: "openknot", -}; - -export function resolveTheme(mode: string): ResolvedTheme { - if (VALID_THEMES.has(mode as ThemeMode)) { - return mode as ThemeMode; +export function getSystemTheme(): ResolvedTheme { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") { + return "dark"; } - return LEGACY_MAP[mode] ?? "dark"; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +export function resolveTheme(mode: ThemeMode): ResolvedTheme { + if (mode === "system") { + return getSystemTheme(); + } + return mode; } diff --git a/ui/src/ui/tool-labels.ts b/ui/src/ui/tool-labels.ts deleted file mode 100644 index e4818c49362..00000000000 --- a/ui/src/ui/tool-labels.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Map raw tool names to human-friendly labels for the chat UI. - * Unknown tools are title-cased with underscores replaced by spaces. - */ - -export const TOOL_LABELS: Record = { - exec: "Run Command", - bash: "Run Command", - read: "Read File", - write: "Write File", - edit: "Edit File", - apply_patch: "Apply Patch", - web_search: "Web Search", - web_fetch: "Fetch Page", - browser: "Browser", - message: "Send Message", - image: "Generate Image", - canvas: "Canvas", - cron: "Cron", - gateway: "Gateway", - nodes: "Nodes", - memory_search: "Search Memory", - memory_get: "Get Memory", - session_status: "Session Status", - sessions_list: "List Sessions", - sessions_history: "Session History", - sessions_send: "Send to Session", - sessions_spawn: "Spawn Session", - agents_list: "List Agents", -}; - -export function friendlyToolName(raw: string): string { - const mapped = TOOL_LABELS[raw]; - if (mapped) { - return mapped; - } - // Title-case fallback: "some_tool_name" → "Some Tool Name" - return raw.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); -} diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index eaf7ca06319..307bae9388f 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -556,35 +556,6 @@ export type StatusSummary = Record; export type HealthSnapshot = Record; -/** Strongly-typed health response from the gateway (richer than HealthSnapshot). */ -export type HealthSummary = { - ok: boolean; - ts: number; - durationMs: number; - heartbeatSeconds: number; - defaultAgentId: string; - agents: Array<{ id: string; name?: string }>; - sessions: { - path: string; - count: number; - recent: Array<{ - key: string; - updatedAt: number | null; - age: number | null; - }>; - }; -}; - -/** A model entry returned by the gateway model-catalog endpoint. */ -export type ModelCatalogEntry = { - id: string; - name: string; - provider: string; - contextWindow?: number; - reasoning?: boolean; - input?: Array<"text" | "image">; -}; - export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; export type LogEntry = { @@ -595,16 +566,3 @@ export type LogEntry = { message?: string | null; meta?: Record | null; }; - -// ── Attention ─────────────────────────────────────── - -export type AttentionSeverity = "error" | "warning" | "info"; - -export type AttentionItem = { - severity: AttentionSeverity; - icon: string; - title: string; - description: string; - href?: string; - external?: boolean; -}; diff --git a/ui/src/ui/views/agents-panels-overview.ts b/ui/src/ui/views/agents-panels-overview.ts deleted file mode 100644 index a19234550b5..00000000000 --- a/ui/src/ui/views/agents-panels-overview.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { html, nothing } from "lit"; -import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult } from "../types.ts"; -import { - agentAvatarHue, - agentBadgeText, - buildModelOptions, - normalizeAgentLabel, - normalizeModelValue, - parseFallbackList, - resolveAgentConfig, - resolveAgentEmoji, - resolveModelFallbacks, - resolveModelLabel, - resolveModelPrimary, -} from "./agents-utils.ts"; -import type { AgentsPanel } from "./agents.ts"; - -export function renderAgentOverview(params: { - agent: AgentsListResult["agents"][number]; - defaultId: string | null; - configForm: Record | null; - agentFilesList: AgentsFilesListResult | null; - agentIdentity: AgentIdentityResult | null; - agentIdentityLoading: boolean; - agentIdentityError: string | null; - configLoading: boolean; - configSaving: boolean; - configDirty: boolean; - onConfigReload: () => void; - onConfigSave: () => void; - onModelChange: (agentId: string, modelId: string | null) => void; - onModelFallbacksChange: (agentId: string, fallbacks: string[]) => void; - onSelectPanel: (panel: AgentsPanel) => void; -}) { - const { - agent, - configForm, - agentFilesList, - agentIdentity, - agentIdentityLoading, - agentIdentityError, - configLoading, - configSaving, - configDirty, - onConfigReload, - onConfigSave, - onModelChange, - onModelFallbacksChange, - onSelectPanel, - } = params; - const config = resolveAgentConfig(configForm, agent.id); - const workspaceFromFiles = - agentFilesList && agentFilesList.agentId === agent.id ? agentFilesList.workspace : null; - const workspace = - workspaceFromFiles || config.entry?.workspace || config.defaults?.workspace || "default"; - const model = config.entry?.model - ? resolveModelLabel(config.entry?.model) - : resolveModelLabel(config.defaults?.model); - const defaultModel = resolveModelLabel(config.defaults?.model); - const modelPrimary = - resolveModelPrimary(config.entry?.model) || (model !== "-" ? normalizeModelValue(model) : null); - const defaultPrimary = - resolveModelPrimary(config.defaults?.model) || - (defaultModel !== "-" ? normalizeModelValue(defaultModel) : null); - const effectivePrimary = modelPrimary ?? defaultPrimary ?? null; - const modelFallbacks = resolveModelFallbacks(config.entry?.model); - const fallbackChips = modelFallbacks ?? []; - const identityName = - agentIdentity?.name?.trim() || - agent.identity?.name?.trim() || - agent.name?.trim() || - config.entry?.name || - "-"; - const resolvedEmoji = resolveAgentEmoji(agent, agentIdentity); - const identityEmoji = resolvedEmoji || "-"; - const skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null; - const skillCount = skillFilter?.length ?? null; - const identityStatus = agentIdentityLoading - ? "Loading…" - : agentIdentityError - ? "Unavailable" - : ""; - const isDefault = Boolean(params.defaultId && agent.id === params.defaultId); - const badge = agentBadgeText(agent.id, params.defaultId); - const hue = agentAvatarHue(agent.id); - const displayName = normalizeAgentLabel(agent); - const subtitle = agent.identity?.theme?.trim() || ""; - const disabled = !configForm || configLoading || configSaving; - - const removeChip = (index: number) => { - const next = fallbackChips.filter((_, i) => i !== index); - onModelFallbacksChange(agent.id, next); - }; - - const handleChipKeydown = (e: KeyboardEvent) => { - const input = e.target as HTMLInputElement; - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - const parsed = parseFallbackList(input.value); - if (parsed.length > 0) { - onModelFallbacksChange(agent.id, [...fallbackChips, ...parsed]); - input.value = ""; - } - } - }; - - return html` -
-
Overview
-
Workspace paths and identity metadata.
- -
-
- ${resolvedEmoji || displayName.slice(0, 1)} -
-
-
${identityName}
-
- ${identityEmoji !== "-" ? html`${identityEmoji}` : nothing} - ${subtitle ? html`${subtitle}` : nothing} - ${badge ? html`${badge}` : nothing} - ${identityStatus ? html`${identityStatus}` : nothing} -
-
-
- -
-
-
Workspace
-
- -
-
-
-
Primary Model
-
${model}
-
-
-
Skills Filter
-
${skillFilter ? `${skillCount} selected` : "all skills"}
-
-
- - ${ - configDirty - ? html` -
You have unsaved config changes.
- ` - : nothing - } - -
-
Model Selection
-
- -
- Fallbacks -
{ - const container = e.currentTarget as HTMLElement; - const input = container.querySelector("input"); - if (input) { - input.focus(); - } - }}> - ${fallbackChips.map( - (chip, i) => html` - - ${chip} - - - `, - )} - { - const input = e.target as HTMLInputElement; - const parsed = parseFallbackList(input.value); - if (parsed.length > 0) { - onModelFallbacksChange(agent.id, [...fallbackChips, ...parsed]); - input.value = ""; - } - }} - /> -
-
-
-
- - -
-
-
- `; -} diff --git a/ui/src/ui/views/agents-panels-status-files.ts b/ui/src/ui/views/agents-panels-status-files.ts index 58ff34782e2..23de4cb96b6 100644 --- a/ui/src/ui/views/agents-panels-status-files.ts +++ b/ui/src/ui/views/agents-panels-status-files.ts @@ -230,7 +230,7 @@ export function renderAgentChannels(params: { const status = summary.total ? `${summary.connected}/${summary.total} connected` : "no accounts"; - const configLabel = summary.configured + const config = summary.configured ? `${summary.configured} configured` : "not configured"; const enabled = summary.total ? `${summary.enabled} enabled` : "disabled"; @@ -243,23 +243,8 @@ export function renderAgentChannels(params: {
${status}
-
${configLabel}
+
${config}
${enabled}
- ${ - summary.configured === 0 - ? html` - - ` - : nothing - } ${ extras.length > 0 ? extras.map( @@ -287,7 +272,6 @@ export function renderAgentCron(params: { loading: boolean; error: string | null; onRefresh: () => void; - onRunNow: (jobId: string) => void; }) { const jobs = params.jobs.filter((job) => job.agentId === params.agentId); return html` @@ -357,12 +341,6 @@ export function renderAgentCron(params: {
${formatCronState(job)}
${formatCronPayload(job)}
-
`, diff --git a/ui/src/ui/views/agents-panels-tools-skills.ts b/ui/src/ui/views/agents-panels-tools-skills.ts index 49da26f34bc..687ec749a62 100644 --- a/ui/src/ui/views/agents-panels-tools-skills.ts +++ b/ui/src/ui/views/agents-panels-tools-skills.ts @@ -301,27 +301,17 @@ export function renderAgentSkills(params: { } -
-
- - - -
+
+ + diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts index 4ea1053d511..ecd2c90f13b 100644 --- a/ui/src/ui/views/agents-utils.ts +++ b/ui/src/ui/views/agents-utils.ts @@ -189,14 +189,6 @@ export function agentBadgeText(agentId: string, defaultId: string | null) { return defaultId && agentId === defaultId ? "default" : null; } -export function agentAvatarHue(id: string): number { - let hash = 0; - for (let i = 0; i < id.length; i += 1) { - hash = (hash * 31 + id.charCodeAt(i)) | 0; - } - return ((hash % 360) + 360) % 360; -} - export function formatBytes(bytes?: number) { if (bytes == null || !Number.isFinite(bytes)) { return "-"; diff --git a/ui/src/ui/views/agents.ts b/ui/src/ui/views/agents.ts index 55a3001abb6..f8cf5cb5f57 100644 --- a/ui/src/ui/views/agents.ts +++ b/ui/src/ui/views/agents.ts @@ -8,7 +8,6 @@ import type { CronStatus, SkillStatusReport, } from "../types.ts"; -import { renderAgentOverview } from "./agents-panels-overview.ts"; import { renderAgentFiles, renderAgentChannels, @@ -16,70 +15,54 @@ import { } from "./agents-panels-status-files.ts"; import { renderAgentTools, renderAgentSkills } from "./agents-panels-tools-skills.ts"; import { - agentAvatarHue, agentBadgeText, buildAgentContext, + buildModelOptions, normalizeAgentLabel, + normalizeModelValue, + parseFallbackList, + resolveAgentConfig, resolveAgentEmoji, + resolveModelFallbacks, + resolveModelLabel, + resolveModelPrimary, } from "./agents-utils.ts"; export type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron"; -export type ConfigState = { - form: Record | null; - loading: boolean; - saving: boolean; - dirty: boolean; -}; - -export type ChannelsState = { - snapshot: ChannelsStatusSnapshot | null; - loading: boolean; - error: string | null; - lastSuccess: number | null; -}; - -export type CronState = { - status: CronStatus | null; - jobs: CronJob[]; - loading: boolean; - error: string | null; -}; - -export type AgentFilesState = { - list: AgentsFilesListResult | null; - loading: boolean; - error: string | null; - active: string | null; - contents: Record; - drafts: Record; - saving: boolean; -}; - -export type AgentSkillsState = { - report: SkillStatusReport | null; - loading: boolean; - error: string | null; - agentId: string | null; - filter: string; -}; - export type AgentsProps = { loading: boolean; error: string | null; agentsList: AgentsListResult | null; selectedAgentId: string | null; activePanel: AgentsPanel; - config: ConfigState; - channels: ChannelsState; - cron: CronState; - agentFiles: AgentFilesState; + configForm: Record | null; + configLoading: boolean; + configSaving: boolean; + configDirty: boolean; + channelsLoading: boolean; + channelsError: string | null; + channelsSnapshot: ChannelsStatusSnapshot | null; + channelsLastSuccess: number | null; + cronLoading: boolean; + cronStatus: CronStatus | null; + cronJobs: CronJob[]; + cronError: string | null; + agentFilesLoading: boolean; + agentFilesError: string | null; + agentFilesList: AgentsFilesListResult | null; + agentFileActive: string | null; + agentFileContents: Record; + agentFileDrafts: Record; + agentFileSaving: boolean; agentIdentityLoading: boolean; agentIdentityError: string | null; agentIdentityById: Record; - agentSkills: AgentSkillsState; - sidebarFilter: string; - onSidebarFilterChange: (value: string) => void; + agentSkillsLoading: boolean; + agentSkillsReport: SkillStatusReport | null; + agentSkillsError: string | null; + agentSkillsAgentId: string | null; + skillsFilter: string; onRefresh: () => void; onSelectAgent: (agentId: string) => void; onSelectPanel: (panel: AgentsPanel) => void; @@ -96,13 +79,20 @@ export type AgentsProps = { onModelFallbacksChange: (agentId: string, fallbacks: string[]) => void; onChannelsRefresh: () => void; onCronRefresh: () => void; - onCronRunNow: (jobId: string) => void; onSkillsFilterChange: (next: string) => void; onSkillsRefresh: () => void; onAgentSkillToggle: (agentId: string, skillName: string, enabled: boolean) => void; onAgentSkillsClear: (agentId: string) => void; onAgentSkillsDisableAll: (agentId: string) => void; - onSetDefault: (agentId: string) => void; +}; + +export type AgentContext = { + workspace: string; + model: string; + identityName: string; + identityEmoji: string; + skillsLabel: string; + isDefault: boolean; }; export function renderAgents(props: AgentsProps) { @@ -113,27 +103,6 @@ export function renderAgents(props: AgentsProps) { ? (agents.find((agent) => agent.id === selectedId) ?? null) : null; - const sidebarFilter = props.sidebarFilter.trim().toLowerCase(); - const filteredAgents = sidebarFilter - ? agents.filter((agent) => { - const label = normalizeAgentLabel(agent).toLowerCase(); - return label.includes(sidebarFilter) || agent.id.toLowerCase().includes(sidebarFilter); - }) - : agents; - - const channelEntryCount = props.channels.snapshot - ? Object.keys(props.channels.snapshot.channelAccounts ?? {}).length - : null; - const cronJobCount = selectedId - ? props.cron.jobs.filter((j) => j.agentId === selectedId).length - : null; - const tabCounts: Record = { - files: props.agentFiles.list?.files?.length ?? null, - skills: props.agentSkills.report?.skills?.length ?? null, - channels: channelEntryCount, - cron: cronJobCount || null, - }; - return html`
@@ -146,21 +115,6 @@ export function renderAgents(props: AgentsProps) { ${props.loading ? "Loading…" : "Refresh"}
- ${ - agents.length > 1 - ? html` - - props.onSidebarFilterChange((e.target as HTMLInputElement).value)} - style="margin-top: 8px;" - /> - ` - : nothing - } ${ props.error ? html`
${props.error}
` @@ -168,23 +122,20 @@ export function renderAgents(props: AgentsProps) { }
${ - filteredAgents.length === 0 + agents.length === 0 ? html` -
${sidebarFilter ? "No matching agents." : "No agents found."}
+
No agents found.
` - : filteredAgents.map((agent) => { + : agents.map((agent) => { const badge = agentBadgeText(agent.id, defaultId); const emoji = resolveAgentEmoji(agent, props.agentIdentityById[agent.id] ?? null); - const hue = agentAvatarHue(agent.id); return html` - ${ - actionsMenuOpen - ? html` -
- - -
- ` - : nothing - } -
-
+ ${badge ? html`${badge}` : nothing}
`; } -function renderAgentTabs( - active: AgentsPanel, - onSelect: (panel: AgentsPanel) => void, - counts: Record, -) { +function renderAgentTabs(active: AgentsPanel, onSelect: (panel: AgentsPanel) => void) { const tabs: Array<{ id: AgentsPanel; label: string }> = [ { id: "overview", label: "Overview" }, { id: "files", label: "Files" }, @@ -428,10 +329,161 @@ function renderAgentTabs( type="button" @click=${() => onSelect(tab.id)} > - ${tab.label}${counts[tab.id] != null ? html`${counts[tab.id]}` : nothing} + ${tab.label} `, )} `; } + +function renderAgentOverview(params: { + agent: AgentsListResult["agents"][number]; + defaultId: string | null; + configForm: Record | null; + agentFilesList: AgentsFilesListResult | null; + agentIdentity: AgentIdentityResult | null; + agentIdentityLoading: boolean; + agentIdentityError: string | null; + configLoading: boolean; + configSaving: boolean; + configDirty: boolean; + onConfigReload: () => void; + onConfigSave: () => void; + onModelChange: (agentId: string, modelId: string | null) => void; + onModelFallbacksChange: (agentId: string, fallbacks: string[]) => void; +}) { + const { + agent, + configForm, + agentFilesList, + agentIdentity, + agentIdentityLoading, + agentIdentityError, + configLoading, + configSaving, + configDirty, + onConfigReload, + onConfigSave, + onModelChange, + onModelFallbacksChange, + } = params; + const config = resolveAgentConfig(configForm, agent.id); + const workspaceFromFiles = + agentFilesList && agentFilesList.agentId === agent.id ? agentFilesList.workspace : null; + const workspace = + workspaceFromFiles || config.entry?.workspace || config.defaults?.workspace || "default"; + const model = config.entry?.model + ? resolveModelLabel(config.entry?.model) + : resolveModelLabel(config.defaults?.model); + const defaultModel = resolveModelLabel(config.defaults?.model); + const modelPrimary = + resolveModelPrimary(config.entry?.model) || (model !== "-" ? normalizeModelValue(model) : null); + const defaultPrimary = + resolveModelPrimary(config.defaults?.model) || + (defaultModel !== "-" ? normalizeModelValue(defaultModel) : null); + const effectivePrimary = modelPrimary ?? defaultPrimary ?? null; + const modelFallbacks = resolveModelFallbacks(config.entry?.model); + const fallbackText = modelFallbacks ? modelFallbacks.join(", ") : ""; + const identityName = + agentIdentity?.name?.trim() || + agent.identity?.name?.trim() || + agent.name?.trim() || + config.entry?.name || + "-"; + const resolvedEmoji = resolveAgentEmoji(agent, agentIdentity); + const identityEmoji = resolvedEmoji || "-"; + const skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null; + const skillCount = skillFilter?.length ?? null; + const identityStatus = agentIdentityLoading + ? "Loading…" + : agentIdentityError + ? "Unavailable" + : ""; + const isDefault = Boolean(params.defaultId && agent.id === params.defaultId); + + return html` +
+
Overview
+
Workspace paths and identity metadata.
+
+
+
Workspace
+
${workspace}
+
+
+
Primary Model
+
${model}
+
+
+
Identity Name
+
${identityName}
+ ${identityStatus ? html`
${identityStatus}
` : nothing} +
+
+
Default
+
${isDefault ? "yes" : "no"}
+
+
+
Identity Emoji
+
${identityEmoji}
+
+
+
Skills Filter
+
${skillFilter ? `${skillCount} selected` : "all skills"}
+
+
+ +
+
Model Selection
+
+ + +
+
+ + +
+
+
+ `; +} diff --git a/ui/src/ui/views/bottom-tabs.ts b/ui/src/ui/views/bottom-tabs.ts deleted file mode 100644 index b8dfbebf39c..00000000000 --- a/ui/src/ui/views/bottom-tabs.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { html } from "lit"; -import { icons } from "../icons.ts"; -import type { Tab } from "../navigation.ts"; - -export type BottomTabsProps = { - activeTab: Tab; - onTabChange: (tab: Tab) => void; -}; - -const BOTTOM_TABS: Array<{ id: Tab; label: string; icon: keyof typeof icons }> = [ - { id: "overview", label: "Dashboard", icon: "barChart" }, - { id: "chat", label: "Chat", icon: "messageSquare" }, - { id: "sessions", label: "Sessions", icon: "fileText" }, - { id: "config", label: "Settings", icon: "settings" }, -]; - -export function renderBottomTabs(props: BottomTabsProps) { - return html` - - `; -} diff --git a/ui/src/ui/views/channels.nostr-profile-form.ts b/ui/src/ui/views/channels.nostr-profile-form.ts index 244236eba78..62e4669f397 100644 --- a/ui/src/ui/views/channels.nostr-profile-form.ts +++ b/ui/src/ui/views/channels.nostr-profile-form.ts @@ -247,7 +247,7 @@ export function renderNostrProfileForm(params: { @click=${callbacks.onSave} ?disabled=${state.saving || !isDirty} > - ${state.saving ? "Saving..." : "Save"} + ${state.saving ? "Saving..." : "Save & Publish"} + > + ${icons.x} + `, )} @@ -328,265 +237,6 @@ function renderAttachmentPreview(props: ChatProps): TemplateResult | typeof noth `; } -function updateSlashMenu(value: string, requestUpdate: () => void): void { - const match = value.match(/^\/(\S*)$/); - if (match) { - const items = getSlashCommandCompletions(match[1]); - slashMenuItems = items; - slashMenuOpen = items.length > 0; - slashMenuIndex = 0; - } else { - slashMenuOpen = false; - slashMenuItems = []; - } - requestUpdate(); -} - -function selectSlashCommand( - cmd: SlashCommandDef, - props: ChatProps, - requestUpdate: () => void, -): void { - const text = `/${cmd.name} `; - props.onDraftChange(text); - slashMenuOpen = false; - slashMenuItems = []; - requestUpdate(); -} - -function tokenEstimate(draft: string): string | null { - if (draft.length < 100) { - return null; - } - return `~${Math.ceil(draft.length / 4)} tokens`; -} - -function startVoice(props: ChatProps, requestUpdate: () => void): void { - const SR = - (window as unknown as Record).webkitSpeechRecognition ?? - (window as unknown as Record).SpeechRecognition; - if (!SR) { - return; - } - const rec = new (SR as new () => Record)(); - rec.continuous = false; - rec.interimResults = true; - rec.lang = "en-US"; - rec.onresult = (event: Record) => { - let transcript = ""; - const results = ( - event as { results: { length: number; [i: number]: { 0: { transcript: string } } } } - ).results; - for (let i = 0; i < results.length; i++) { - transcript += results[i][0].transcript; - } - props.onDraftChange(transcript); - }; - (rec as unknown as EventTarget).addEventListener("end", () => { - voiceActive = false; - recognition = null; - requestUpdate(); - }); - (rec as unknown as EventTarget).addEventListener("error", () => { - voiceActive = false; - recognition = null; - requestUpdate(); - }); - (rec as { start: () => void }).start(); - recognition = rec; - voiceActive = true; - requestUpdate(); -} - -function stopVoice(requestUpdate: () => void): void { - if (recognition && typeof recognition.stop === "function") { - recognition.stop(); - } - recognition = null; - voiceActive = false; - requestUpdate(); -} - -function exportMarkdown(props: ChatProps): void { - const history = Array.isArray(props.messages) ? props.messages : []; - if (history.length === 0) { - return; - } - const lines: string[] = [`# Chat with ${props.assistantName}`, ""]; - for (const msg of history) { - const m = msg as Record; - const role = m.role === "user" ? "You" : m.role === "assistant" ? props.assistantName : "Tool"; - const content = typeof m.content === "string" ? m.content : ""; - const ts = typeof m.timestamp === "number" ? new Date(m.timestamp).toISOString() : ""; - lines.push(`## ${role}${ts ? ` (${ts})` : ""}`, "", content, ""); - } - const blob = new Blob([lines.join("\n")], { type: "text/markdown" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `chat-${props.assistantName}-${Date.now()}.md`; - a.click(); - URL.revokeObjectURL(url); -} - -function renderWelcomeState(props: ChatProps): TemplateResult { - const name = props.assistantName || "Assistant"; - const avatar = props.assistantAvatar ?? props.assistantAvatarUrl; - const initials = name.slice(0, 2).toUpperCase(); - - return html` -
-
- ${ - avatar - ? html`${name}` - : html`
${initials}
` - } -

${name}

-
- ${icons.spark} Ready to chat -
-

- Type a message below · / for commands -

-
- `; -} - -function renderSearchBar(requestUpdate: () => void): TemplateResult | typeof nothing { - if (!searchOpen) { - return nothing; - } - return html` - - `; -} - -function renderPinnedSection( - props: ChatProps, - pinned: PinnedMessages, - requestUpdate: () => void, -): TemplateResult | typeof nothing { - const messages = Array.isArray(props.messages) ? props.messages : []; - const entries: Array<{ index: number; text: string; role: string }> = []; - for (const idx of pinned.indices) { - const msg = messages[idx] as Record | undefined; - if (!msg) { - continue; - } - const text = typeof msg.content === "string" ? msg.content : ""; - const role = typeof msg.role === "string" ? msg.role : "unknown"; - entries.push({ index: idx, text, role }); - } - if (entries.length === 0) { - return nothing; - } - return html` -
- - ${ - pinnedExpanded - ? html` -
- ${entries.map( - ({ index, text, role }) => html` -
- ${role === "user" ? "You" : "Assistant"} - ${text.slice(0, 100)}${text.length > 100 ? "..." : ""} - -
- `, - )} -
- ` - : nothing - } -
- `; -} - -function renderSlashMenu( - requestUpdate: () => void, - props: ChatProps, -): TemplateResult | typeof nothing { - if (!slashMenuOpen || slashMenuItems.length === 0) { - return nothing; - } - - const grouped = new Map< - SlashCommandCategory, - Array<{ cmd: SlashCommandDef; globalIdx: number }> - >(); - for (let i = 0; i < slashMenuItems.length; i++) { - const cmd = slashMenuItems[i]; - const cat = cmd.category ?? "session"; - let list = grouped.get(cat); - if (!list) { - list = []; - grouped.set(cat, list); - } - list.push({ cmd, globalIdx: i }); - } - - const sections: TemplateResult[] = []; - for (const [cat, entries] of grouped) { - sections.push(html` -
-
${CATEGORY_LABELS[cat]}
- ${entries.map( - ({ cmd, globalIdx }) => html` -
selectSlashCommand(cmd, props, requestUpdate)} - @mouseenter=${() => { - slashMenuIndex = globalIdx; - requestUpdate(); - }} - > - ${cmd.icon ? html`${icons[cmd.icon]}` : nothing} - /${cmd.name} - ${cmd.args ? html`${cmd.args}` : nothing} - ${cmd.description} -
- `, - )} -
- `); - } - - return html`
${sections}
`; -} - export function renderChat(props: ChatProps) { const canCompose = props.connected; const isBusy = props.sending || props.stream !== null; @@ -598,35 +248,16 @@ export function renderChat(props: ChatProps) { name: props.assistantName, avatar: props.assistantAvatar ?? props.assistantAvatarUrl ?? null, }; - const pinned = getPinnedMessages(props.sessionKey); - const deleted = getDeletedMessages(props.sessionKey); - const inputHistory = getInputHistory(props.sessionKey); + const hasAttachments = (props.attachments?.length ?? 0) > 0; - const tokens = tokenEstimate(props.draft); - - const hasVoice = - typeof (window as unknown as Record).webkitSpeechRecognition !== "undefined" || - typeof (window as unknown as Record).SpeechRecognition !== "undefined"; - - const placeholder = props.connected + const composePlaceholder = props.connected ? hasAttachments ? "Add a message or paste more images..." - : `Message ${props.assistantName || "agent"} (Enter to send)` - : "Connect to the gateway to start chatting..."; - - // We need a requestUpdate shim since we're in functional mode: - // the host Lit component will re-render on state change anyway, - // so we trigger by calling onDraftChange with current value. - const requestUpdate = () => { - props.onDraftChange(props.draft); - }; + : "Message (↩ to send, Shift+↩ for line breaks, paste images)" + : "Connect to the gateway to start chatting…"; const splitRatio = props.splitRatio ?? 0.6; const sidebarOpen = Boolean(props.sidebarOpen && props.onCloseSidebar); - - const chatItems = buildChatItems(props); - const isEmpty = chatItems.length === 0 && !props.loading; - const thread = html`
Loading chat...
- ` - : nothing - } - ${isEmpty && !searchOpen ? renderWelcomeState(props) : nothing} - ${ - isEmpty && searchOpen - ? html` -
No matching messages
+
Loading chat…
` : nothing } ${repeat( - chatItems, + buildChatItems(props), (item) => item.key, (item) => { if (item.kind === "divider") { @@ -662,9 +285,11 @@ export function renderChat(props: ChatProps) { `; } + if (item.kind === "reading-indicator") { return renderReadingIndicatorGroup(assistantIdentity); } + if (item.kind === "stream") { return renderStreamingGroup( item.text, @@ -673,117 +298,26 @@ export function renderChat(props: ChatProps) { assistantIdentity, ); } + if (item.kind === "group") { - if (deleted.has(item.key)) { - return nothing; - } return renderMessageGroup(item, { onOpenSidebar: props.onOpenSidebar, showReasoning, assistantName: props.assistantName, assistantAvatar: assistantIdentity.avatar, - onDelete: () => { - deleted.delete(item.key); - requestUpdate(); - }, }); } + return nothing; }, )} `; - const handleKeyDown = (e: KeyboardEvent) => { - // Slash menu navigation - if (slashMenuOpen && slashMenuItems.length > 0) { - const len = slashMenuItems.length; - switch (e.key) { - case "ArrowDown": - e.preventDefault(); - slashMenuIndex = (slashMenuIndex + 1) % len; - requestUpdate(); - return; - case "ArrowUp": - e.preventDefault(); - slashMenuIndex = (slashMenuIndex - 1 + len) % len; - requestUpdate(); - return; - case "Enter": - case "Tab": - e.preventDefault(); - selectSlashCommand(slashMenuItems[slashMenuIndex], props, requestUpdate); - return; - case "Escape": - e.preventDefault(); - slashMenuOpen = false; - requestUpdate(); - return; - } - } - - // Input history (only when input is empty) - if (!props.draft.trim()) { - if (e.key === "ArrowUp") { - const prev = inputHistory.up(); - if (prev !== null) { - e.preventDefault(); - props.onDraftChange(prev); - } - return; - } - if (e.key === "ArrowDown") { - const next = inputHistory.down(); - e.preventDefault(); - props.onDraftChange(next ?? ""); - return; - } - } - - // Cmd+F for search - if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key === "f") { - e.preventDefault(); - searchOpen = !searchOpen; - if (!searchOpen) { - searchQuery = ""; - } - requestUpdate(); - return; - } - - // Send on Enter (without shift) - if (e.key === "Enter" && !e.shiftKey) { - if (e.isComposing || e.keyCode === 229) { - return; - } - if (!props.connected) { - return; - } - e.preventDefault(); - if (canCompose) { - if (props.draft.trim()) { - inputHistory.push(props.draft); - } - props.onSend(); - } - } - }; - - const handleInput = (e: Event) => { - const target = e.target as HTMLTextAreaElement; - adjustTextareaHeight(target); - props.onDraftChange(target.value); - updateSlashMenu(target.value, requestUpdate); - inputHistory.reset(); - }; - return html` -
handleDrop(e, props)} - @dragover=${(e: DragEvent) => e.preventDefault()} - > +
${props.disabledReason ? html`
${props.disabledReason}
` : nothing} + ${props.error ? html`
${props.error}
` : nothing} ${ @@ -802,12 +336,9 @@ export function renderChat(props: ChatProps) { : nothing } - ${renderSearchBar(requestUpdate)} - ${renderPinnedSection(props, pinned, requestUpdate)} - - ${renderAgentBar(props)} - -
+
- ${icons.arrowDown} New messages + New messages ${icons.arrowDown} ` : nothing } - -
- ${renderSlashMenu(requestUpdate, props)} +
${renderAttachmentPreview(props)} - - handleFileSelect(e, props)} - /> - - - -
-
- - - ${ - hasVoice - ? html` - - ` - : nothing - } - - ${tokens ? html`${tokens}` : nothing} -
- -
- - - ${ - props.messages.length > 0 - ? html` - - - - - ` - : nothing - } - - ${ - canAbort && isBusy - ? html` - - ` - : html` - - ` - }
@@ -1010,83 +479,6 @@ export function renderChat(props: ChatProps) { `; } -function renderAgentBar(props: ChatProps) { - const agents = props.agentsList?.agents ?? []; - if (agents.length <= 1 && !props.sessions?.sessions?.length) { - return nothing; - } - - // Filter sessions for current agent - const agentSessions = (props.sessions?.sessions ?? []).filter((s) => { - const key = s.key ?? ""; - return ( - key.includes(`:${props.currentAgentId}:`) || key.startsWith(`agent:${props.currentAgentId}:`) - ); - }); - - return html` -
-
- ${ - agents.length > 1 - ? html` - - ` - : html`${agents[0]?.identity?.name || agents[0]?.name || props.currentAgentId}` - } - ${ - agentSessions.length > 0 - ? html` -
- - ${icons.fileText} - Sessions (${agentSessions.length}) - -
- ${agentSessions.map( - (s) => html` - - `, - )} -
-
- ` - : nothing - } -
-
- ${ - props.onNavigateToAgent - ? html` - - ` - : nothing - } -
-
- `; -} - const CHAT_HISTORY_RENDER_LIMIT = 200; function groupMessages(items: ChatItem[]): Array { @@ -1168,14 +560,6 @@ function buildChatItems(props: ChatProps): Array { continue; } - // Apply search filter if active - if (searchOpen && searchQuery.trim()) { - const text = typeof normalized.content === "string" ? normalized.content : ""; - if (!text.toLowerCase().includes(searchQuery.toLowerCase())) { - continue; - } - } - items.push({ kind: "message", key: messageKey(msg, i), diff --git a/ui/src/ui/views/command-palette.ts b/ui/src/ui/views/command-palette.ts deleted file mode 100644 index 639af836ab1..00000000000 --- a/ui/src/ui/views/command-palette.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { html, nothing } from "lit"; -import { t } from "../../i18n/index.ts"; -import { icons, type IconName } from "../icons.ts"; - -type PaletteItem = { - id: string; - label: string; - icon: IconName; - category: "search" | "navigation" | "skills"; - action: string; - description?: string; -}; - -const PALETTE_ITEMS: PaletteItem[] = [ - { - id: "status", - label: "/status", - icon: "radio", - category: "search", - action: "/status", - description: "Show current status", - }, - { - id: "models", - label: "/model", - icon: "monitor", - category: "search", - action: "/model", - description: "Show/set model", - }, - { - id: "usage", - label: "/usage", - icon: "barChart", - category: "search", - action: "/usage", - description: "Show usage", - }, - { - id: "think", - label: "/think", - icon: "brain", - category: "search", - action: "/think", - description: "Set thinking level", - }, - { - id: "reset", - label: "/reset", - icon: "loader", - category: "search", - action: "/reset", - description: "Reset session", - }, - { - id: "help", - label: "/help", - icon: "book", - category: "search", - action: "/help", - description: "Show help", - }, - { - id: "nav-overview", - label: "Overview", - icon: "barChart", - category: "navigation", - action: "nav:overview", - }, - { - id: "nav-sessions", - label: "Sessions", - icon: "fileText", - category: "navigation", - action: "nav:sessions", - }, - { - id: "nav-cron", - label: "Scheduled", - icon: "scrollText", - category: "navigation", - action: "nav:cron", - }, - { id: "nav-skills", label: "Skills", icon: "zap", category: "navigation", action: "nav:skills" }, - { - id: "nav-config", - label: "Settings", - icon: "settings", - category: "navigation", - action: "nav:config", - }, - { - id: "nav-agents", - label: "Agents", - icon: "folder", - category: "navigation", - action: "nav:agents", - }, - { - id: "skill-shell", - label: "Shell Command", - icon: "monitor", - category: "skills", - action: "/skill shell", - description: "Run shell", - }, - { - id: "skill-debug", - label: "Debug Mode", - icon: "bug", - category: "skills", - action: "/verbose full", - description: "Toggle debug", - }, -]; - -export type CommandPaletteProps = { - open: boolean; - query: string; - activeIndex: number; - onToggle: () => void; - onQueryChange: (query: string) => void; - onActiveIndexChange: (index: number) => void; - onNavigate: (tab: string) => void; - onSlashCommand: (command: string) => void; -}; - -function filteredItems(query: string): PaletteItem[] { - if (!query) { - return PALETTE_ITEMS; - } - const q = query.toLowerCase(); - return PALETTE_ITEMS.filter( - (item) => - item.label.toLowerCase().includes(q) || - (item.description?.toLowerCase().includes(q) ?? false), - ); -} - -function groupItems(items: PaletteItem[]): Array<[string, PaletteItem[]]> { - const map = new Map(); - for (const item of items) { - const group = map.get(item.category) ?? []; - group.push(item); - map.set(item.category, group); - } - return [...map.entries()]; -} - -function selectItem(item: PaletteItem, props: CommandPaletteProps) { - if (item.action.startsWith("nav:")) { - props.onNavigate(item.action.slice(4)); - } else { - props.onSlashCommand(item.action); - } - props.onToggle(); -} - -function handleKeydown(e: KeyboardEvent, props: CommandPaletteProps) { - const items = filteredItems(props.query); - switch (e.key) { - case "ArrowDown": - e.preventDefault(); - props.onActiveIndexChange(Math.min(props.activeIndex + 1, items.length - 1)); - break; - case "ArrowUp": - e.preventDefault(); - props.onActiveIndexChange(Math.max(props.activeIndex - 1, 0)); - break; - case "Enter": - e.preventDefault(); - if (items[props.activeIndex]) { - selectItem(items[props.activeIndex], props); - } - break; - case "Escape": - e.preventDefault(); - props.onToggle(); - break; - } -} - -const CATEGORY_LABELS: Record = { - search: "Search", - navigation: "Navigation", - skills: "Skills", -}; - -export function renderCommandPalette(props: CommandPaletteProps) { - if (!props.open) { - return nothing; - } - - const items = filteredItems(props.query); - const grouped = groupItems(items); - - return html` -
props.onToggle()}> -
e.stopPropagation()}> - { - props.onQueryChange((e.target as HTMLInputElement).value); - props.onActiveIndexChange(0); - }} - @keydown=${(e: KeyboardEvent) => handleKeydown(e, props)} - autofocus - /> -
- ${ - grouped.length === 0 - ? html`
${t("overview.palette.noResults")}
` - : grouped.map( - ([category, groupedItems]) => html` -
${CATEGORY_LABELS[category] ?? category}
- ${groupedItems.map((item) => { - const globalIndex = items.indexOf(item); - const isActive = globalIndex === props.activeIndex; - return html` -
selectItem(item, props)} - @mouseenter=${() => props.onActiveIndexChange(globalIndex)} - > - ${icons[item.icon]} - ${item.label} - ${ - item.description - ? html`${item.description}` - : nothing - } -
- `; - })} - `, - ) - } -
-
-
- `; -} diff --git a/ui/src/ui/views/config-form.analyze.ts b/ui/src/ui/views/config-form.analyze.ts index 261f4fc1618..9bf17dcde95 100644 --- a/ui/src/ui/views/config-form.analyze.ts +++ b/ui/src/ui/views/config-form.analyze.ts @@ -118,47 +118,12 @@ function normalizeSchemaNode( }; } -function mergeAllOf(schema: JsonSchema, path: Array): ConfigSchemaAnalysis | null { - const branches = schema.allOf; - if (!branches || branches.length === 0) { - return null; - } - const merged: JsonSchema = { ...schema, allOf: undefined }; - for (const branch of branches) { - if (!branch || typeof branch !== "object") { - return null; - } - if (branch.type) { - merged.type = merged.type ?? branch.type; - } - if (branch.properties) { - merged.properties = { ...merged.properties, ...branch.properties }; - } - if (branch.items && !merged.items) { - merged.items = branch.items; - } - if (branch.enum) { - merged.enum = branch.enum; - } - if (branch.description && !merged.description) { - merged.description = branch.description; - } - if (branch.title && !merged.title) { - merged.title = branch.title; - } - if (branch.default !== undefined && merged.default === undefined) { - merged.default = branch.default; - } - } - return normalizeSchemaNode(merged, path); -} - function normalizeUnion( schema: JsonSchema, path: Array, ): ConfigSchemaAnalysis | null { if (schema.allOf) { - return mergeAllOf(schema, path); + return null; } const union = schema.anyOf ?? schema.oneOf; if (!union) { @@ -216,7 +181,7 @@ function normalizeUnion( }; } - if (remaining.length === 1 && literals.length === 0) { + if (remaining.length === 1) { const res = normalizeSchemaNode(remaining[0], path); if (res.schema) { res.schema.nullable = nullable || res.schema.nullable; @@ -224,41 +189,6 @@ function normalizeUnion( return res; } - // Literals + single typed remainder (e.g. boolean | enum["off","partial"]): - // merge literals into an enum on the combined schema so segmented/select renders all options. - if (remaining.length === 1 && literals.length > 0) { - const remType = schemaType(remaining[0]); - if (remType === "boolean") { - const all = [true, false, ...literals]; - const unique: unknown[] = []; - for (const v of all) { - if (!unique.some((e) => Object.is(e, v))) { - unique.push(v); - } - } - return { - schema: { - ...schema, - enum: unique, - nullable, - anyOf: undefined, - oneOf: undefined, - allOf: undefined, - }, - unsupportedPaths: [], - }; - } - // Single remaining primitive — pass through as-is so the renderer picks the right widget - const primitiveTypes = new Set(["string", "number", "integer"]); - if (remType && primitiveTypes.has(remType)) { - const res = normalizeSchemaNode(remaining[0], path); - if (res.schema) { - res.schema.nullable = nullable || res.schema.nullable; - } - return res; - } - } - const primitiveTypes = new Set(["string", "number", "integer", "boolean"]); if ( remaining.length > 0 && @@ -274,9 +204,5 @@ function normalizeUnion( }; } - // Fallback: pass the schema through and let the renderer show a JSON textarea - return { - schema: { ...schema, nullable }, - unsupportedPaths: [], - }; + return null; } diff --git a/ui/src/ui/views/config-form.node.ts b/ui/src/ui/views/config-form.node.ts index ff24a861fe4..cd567d5e662 100644 --- a/ui/src/ui/views/config-form.node.ts +++ b/ui/src/ui/views/config-form.node.ts @@ -27,44 +27,6 @@ function jsonValue(value: unknown): string { } } -function renderJsonFallback(params: { - label: string; - help: string | undefined; - value: unknown; - path: Array; - disabled: boolean; - showLabel: boolean; - onPatch: (path: Array, value: unknown) => void; -}): TemplateResult { - const { label, help, value, path, disabled, showLabel, onPatch } = params; - const display = jsonValue(value); - return html` -
- ${showLabel ? html`` : nothing} - ${help ? html`
${help}
` : nothing} - -
- `; -} - // SVG Icons as template literals const icons = { chevronDown: html` @@ -151,7 +113,10 @@ export function renderNode(params: { const key = pathKey(path); if (unsupported.has(key)) { - return renderJsonFallback({ label, help, value, path, disabled, onPatch, showLabel }); + return html`
+
${label}
+
Unsupported schema node. Use Raw mode.
+
`; } // Handle anyOf/oneOf unions @@ -317,8 +282,13 @@ export function renderNode(params: { return renderTextInput({ ...params, inputType: "text" }); } - // Fallback — render a JSON textarea for types the form renderer doesn't know about - return renderJsonFallback({ label, help, value, path, disabled, onPatch, showLabel }); + // Fallback + return html` +
+
${label}
+
Unsupported type: ${type}. Use Raw mode.
+
+ `; } function renderTextInput(params: { diff --git a/ui/src/ui/views/config.browser.test.ts b/ui/src/ui/views/config.browser.test.ts index 80969272330..cdb7fc195c4 100644 --- a/ui/src/ui/views/config.browser.test.ts +++ b/ui/src/ui/views/config.browser.test.ts @@ -25,7 +25,6 @@ describe("config view", () => { searchQuery: "", activeSection: null, activeSubsection: null, - streamMode: false, onRawChange: vi.fn(), onFormModeChange: vi.fn(), onFormPatch: vi.fn(), @@ -38,7 +37,7 @@ describe("config view", () => { onSubsectionChange: vi.fn(), }); - it("allows save with mixed union schemas", () => { + it("allows save when form is unsafe", () => { const container = document.createElement("div"); render( renderConfig({ diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 0be5a47d37a..221f31e0050 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -1,5 +1,4 @@ import { html, nothing } from "lit"; -import { icons } from "../icons.ts"; import type { ConfigUiHints } from "../types.ts"; import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared.ts"; import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./config-form.ts"; @@ -23,7 +22,6 @@ export type ConfigProps = { searchQuery: string; activeSection: string | null; activeSubsection: string | null; - streamMode: boolean; onRawChange: (next: string) => void; onFormModeChange: (mode: "form" | "raw") => void; onFormPatch: (path: Array, value: unknown) => void; @@ -385,44 +383,6 @@ function truncateValue(value: unknown, maxLen = 40): string { return str.slice(0, maxLen - 3) + "..."; } -const SENSITIVE_KEY_RE = /token|password|secret|api.?key/i; -const SENSITIVE_KEY_WHITELIST_RE = - /maxtokens|maxoutputtokens|maxinputtokens|maxcompletiontokens|contexttokens|totaltokens|tokencount|tokenlimit|tokenbudget|passwordfile/i; - -function countSensitiveValues(formValue: Record | null): number { - if (!formValue) { - return 0; - } - let count = 0; - function walk(obj: unknown, key?: string) { - if (obj == null) { - return; - } - if (typeof obj === "object" && !Array.isArray(obj)) { - for (const [k, v] of Object.entries(obj as Record)) { - walk(v, k); - } - } else if (Array.isArray(obj)) { - for (const item of obj) { - walk(item); - } - } else if ( - key && - typeof obj === "string" && - SENSITIVE_KEY_RE.test(key) && - !SENSITIVE_KEY_WHITELIST_RE.test(key) - ) { - if (obj.trim() && !/^\$\{[^}]*\}$/.test(obj.trim())) { - count++; - } - } - } - walk(formValue); - return count; -} - -let rawRevealed = false; - export function renderConfig(props: ConfigProps) { const validity = props.valid == null ? "unknown" : props.valid ? "valid" : "invalid"; const analysis = analyzeConfigSchema(props.schema); @@ -689,32 +649,6 @@ export function renderConfig(props: ConfigProps) { : nothing }
- ${ - props.activeSection === "env" - ? html` - - ` - : nothing - }
` : nothing @@ -748,7 +682,7 @@ export function renderConfig(props: ConfigProps) { } -
+
${ props.formMode === "form" ? html` @@ -782,43 +716,16 @@ export function renderConfig(props: ConfigProps) { : nothing } ` - : (() => { - const sensitiveCount = countSensitiveValues(props.formValue); - const blurred = sensitiveCount > 0 && (props.streamMode || !rawRevealed); - return html` - - `; - })() + : html` + + ` }
diff --git a/ui/src/ui/views/cron.ts b/ui/src/ui/views/cron.ts index 89527f83a02..e5cc32408ea 100644 --- a/ui/src/ui/views/cron.ts +++ b/ui/src/ui/views/cron.ts @@ -333,7 +333,7 @@ export function renderCron(props: CronProps) {
No runs yet.
` : html` -
+
${orderedRuns.map((entry) => renderRun(entry, props.basePath))}
` diff --git a/ui/src/ui/views/debug.ts b/ui/src/ui/views/debug.ts index 6a03073726f..22ee3bce20f 100644 --- a/ui/src/ui/views/debug.ts +++ b/ui/src/ui/views/debug.ts @@ -1,13 +1,12 @@ import { html, nothing } from "lit"; import type { EventLogEntry } from "../app-events.ts"; import { formatEventPayload } from "../presenter.ts"; -import type { HealthSummary, ModelCatalogEntry } from "../types.ts"; export type DebugProps = { loading: boolean; status: Record | null; - health: HealthSummary | null; - models: ModelCatalogEntry[]; + health: Record | null; + models: unknown[]; heartbeat: unknown; eventLog: EventLogEntry[]; callMethod: string; diff --git a/ui/src/ui/views/instances.ts b/ui/src/ui/views/instances.ts index b805b7ea444..df5fe5fd4fe 100644 --- a/ui/src/ui/views/instances.ts +++ b/ui/src/ui/views/instances.ts @@ -1,6 +1,5 @@ import { html, nothing } from "lit"; -import { icons } from "../icons.ts"; -import { formatPresenceAge } from "../presenter.ts"; +import { formatPresenceAge, formatPresenceSummary } from "../presenter.ts"; import type { PresenceEntry } from "../types.ts"; export type InstancesProps = { @@ -8,15 +7,10 @@ export type InstancesProps = { entries: PresenceEntry[]; lastError: string | null; statusMessage: string | null; - streamMode: boolean; onRefresh: () => void; }; -let hostsRevealed = false; - export function renderInstances(props: InstancesProps) { - const masked = props.streamMode || !hostsRevealed; - return html`
@@ -24,24 +18,9 @@ export function renderInstances(props: InstancesProps) {
Connected Instances
Presence beacons from the gateway and clients.
-
- - -
+
${ props.lastError @@ -63,18 +42,16 @@ export function renderInstances(props: InstancesProps) { ? html`
No instances reported yet.
` - : props.entries.map((entry) => renderEntry(entry, masked)) + : props.entries.map((entry) => renderEntry(entry)) }
`; } -function renderEntry(entry: PresenceEntry, masked: boolean) { +function renderEntry(entry: PresenceEntry) { const lastInput = entry.lastInputSeconds != null ? `${entry.lastInputSeconds}s ago` : "n/a"; const mode = entry.mode ?? "unknown"; - const host = entry.host ?? "unknown host"; - const ip = entry.ip ?? null; const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : []; const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : []; const scopesLabel = @@ -86,12 +63,8 @@ function renderEntry(entry: PresenceEntry, masked: boolean) { return html`
-
- ${host} -
-
- ${ip ? html`${ip} ` : nothing}${mode} ${entry.version ?? ""} -
+
${entry.host ?? "unknown host"}
+
${formatPresenceSummary(entry)}
${mode} ${roles.map((role) => html`${role}`)} diff --git a/ui/src/ui/views/login-gate.ts b/ui/src/ui/views/login-gate.ts deleted file mode 100644 index 58b0033d254..00000000000 --- a/ui/src/ui/views/login-gate.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { html } from "lit"; -import { t } from "../../i18n/index.ts"; -import { renderThemeToggle } from "../app-render.helpers.ts"; -import type { AppViewState } from "../app-view-state.ts"; -import { normalizeBasePath } from "../navigation.ts"; - -export function renderLoginGate(state: AppViewState) { - const basePath = normalizeBasePath(state.basePath ?? ""); - const faviconSrc = basePath ? `${basePath}/favicon.svg` : "/favicon.svg"; - - return html` - - `; -} diff --git a/ui/src/ui/views/overview-attention.ts b/ui/src/ui/views/overview-attention.ts deleted file mode 100644 index e6762f3e2be..00000000000 --- a/ui/src/ui/views/overview-attention.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { html, nothing } from "lit"; -import { t } from "../../i18n/index.ts"; -import { icons, type IconName } from "../icons.ts"; -import type { AttentionItem } from "../types.ts"; - -export type OverviewAttentionProps = { - items: AttentionItem[]; -}; - -function severityClass(severity: string) { - if (severity === "error") { - return "danger"; - } - if (severity === "warning") { - return "warn"; - } - return ""; -} - -function attentionIcon(name: string) { - if (name in icons) { - return icons[name as IconName]; - } - return icons.radio; -} - -export function renderOverviewAttention(props: OverviewAttentionProps) { - if (props.items.length === 0) { - return nothing; - } - - return html` -
-
${t("overview.attention.title")}
-
- ${props.items.map( - (item) => html` -
- ${attentionIcon(item.icon)} -
-
${item.title}
-
${item.description}
-
- ${ - item.href - ? html`${t("common.docs")}` - : nothing - } -
- `, - )} -
-
- `; -} diff --git a/ui/src/ui/views/overview-cards.ts b/ui/src/ui/views/overview-cards.ts deleted file mode 100644 index 3d394a1df11..00000000000 --- a/ui/src/ui/views/overview-cards.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { html, nothing, type TemplateResult } from "lit"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { t } from "../../i18n/index.ts"; -import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts"; -import { icons } from "../icons.ts"; -import { formatNextRun } from "../presenter.ts"; -import type { - SessionsUsageResult, - SessionsListResult, - SkillStatusReport, - CronJob, - CronStatus, -} from "../types.ts"; - -export type OverviewCardsProps = { - usageResult: SessionsUsageResult | null; - sessionsResult: SessionsListResult | null; - skillsReport: SkillStatusReport | null; - cronJobs: CronJob[]; - cronStatus: CronStatus | null; - presenceCount: number; - redacted: boolean; - onNavigate: (tab: string) => void; -}; - -function redact(value: string, redacted: boolean) { - return redacted ? "••••••" : value; -} - -const DIGIT_RUN = /\d{3,}/g; - -function blurDigits(value: string): TemplateResult { - const escaped = value.replace(/&/g, "&").replace(//g, ">"); - const blurred = escaped.replace(DIGIT_RUN, (m) => `${m}`); - return html`${unsafeHTML(blurred)}`; -} - -export function renderOverviewCards(props: OverviewCardsProps) { - const totals = props.usageResult?.totals; - const totalCost = formatCost(totals?.totalCost); - const totalTokens = formatTokens(totals?.totalTokens); - const totalMessages = totals ? String(props.usageResult?.aggregates?.messages?.total ?? 0) : "0"; - const sessionCount = props.sessionsResult?.count ?? null; - - const skills = props.skillsReport?.skills ?? []; - const enabledSkills = skills.filter((s) => !s.disabled).length; - const blockedSkills = skills.filter((s) => s.blockedByAllowlist).length; - const totalSkills = skills.length; - - const cronEnabled = props.cronStatus?.enabled ?? null; - const cronNext = props.cronStatus?.nextWakeAtMs ?? null; - const cronJobCount = props.cronJobs.length; - const failedCronCount = props.cronJobs.filter((j) => j.state?.lastStatus === "error").length; - - return html` -
-
props.onNavigate("usage")}> -
-
${icons.barChart}
-
-
${t("overview.cards.cost")}
-
${redact(totalCost, props.redacted)}
-
${redact(`${totalTokens} tokens · ${totalMessages} msgs`, props.redacted)}
-
-
-
-
props.onNavigate("sessions")}> -
-
${icons.fileText}
-
-
${t("overview.stats.sessions")}
-
${sessionCount ?? t("common.na")}
-
${t("overview.stats.sessionsHint")}
-
-
-
-
props.onNavigate("skills")}> -
-
${icons.zap}
-
-
${t("overview.cards.skills")}
-
${enabledSkills}/${totalSkills}
-
${blockedSkills > 0 ? `${blockedSkills} blocked` : `${enabledSkills} active`}
-
-
-
-
props.onNavigate("cron")}> -
-
${icons.scrollText}
-
-
${t("overview.stats.cron")}
-
- ${cronEnabled == null ? t("common.na") : cronEnabled ? `${cronJobCount} jobs` : t("common.disabled")} -
-
- ${ - failedCronCount > 0 - ? html`${failedCronCount} failed` - : nothing - } - ${cronNext ? t("overview.stats.cronNext", { time: formatNextRun(cronNext) }) : ""} -
-
-
-
-
- - ${ - props.sessionsResult && props.sessionsResult.sessions.length > 0 - ? html` -
-
${t("overview.cards.recentSessions")}
-
- ${props.sessionsResult.sessions.slice(0, 5).map( - (s) => html` -
- ${props.redacted ? redact(s.displayName || s.label || s.key, true) : blurDigits(s.displayName || s.label || s.key)} - ${s.model ?? ""} - ${s.updatedAt ? formatRelativeTimestamp(s.updatedAt) : ""} -
- `, - )} -
-
- ` - : nothing - } - `; -} diff --git a/ui/src/ui/views/overview-event-log.ts b/ui/src/ui/views/overview-event-log.ts deleted file mode 100644 index f4636d3ec27..00000000000 --- a/ui/src/ui/views/overview-event-log.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { html, nothing } from "lit"; -import { t } from "../../i18n/index.ts"; -import type { EventLogEntry } from "../app-events.ts"; -import { icons } from "../icons.ts"; -import { formatEventPayload } from "../presenter.ts"; - -export type OverviewEventLogProps = { - events: EventLogEntry[]; - redacted: boolean; -}; - -export function renderOverviewEventLog(props: OverviewEventLogProps) { - if (props.events.length === 0) { - return nothing; - } - - const visible = props.events.slice(0, 20); - - return html` -
- - ${icons.radio} - ${t("overview.eventLog.title")} - ${props.events.length} - -
- ${visible.map( - (entry) => html` -
- ${new Date(entry.ts).toLocaleTimeString()} - ${entry.event} - ${ - entry.payload - ? html`${formatEventPayload(entry.payload).slice(0, 120)}` - : nothing - } -
- `, - )} -
-
- `; -} diff --git a/ui/src/ui/views/overview-log-tail.ts b/ui/src/ui/views/overview-log-tail.ts deleted file mode 100644 index 72c3c981c2f..00000000000 --- a/ui/src/ui/views/overview-log-tail.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { html, nothing } from "lit"; -import { t } from "../../i18n/index.ts"; -import { icons } from "../icons.ts"; - -export type OverviewLogTailProps = { - lines: string[]; - redacted: boolean; - onRefreshLogs: () => void; -}; - -export function renderOverviewLogTail(props: OverviewLogTailProps) { - if (props.lines.length === 0) { - return nothing; - } - - return html` -
- - ${icons.scrollText} - ${t("overview.logTail.title")} - ${props.lines.length} - { - e.preventDefault(); - e.stopPropagation(); - props.onRefreshLogs(); - }} - >${icons.loader} - -
${
-        props.redacted ? "[log hidden]" : props.lines.slice(-50).join("\n")
-      }
-
- `; -} diff --git a/ui/src/ui/views/overview-quick-actions.ts b/ui/src/ui/views/overview-quick-actions.ts deleted file mode 100644 index b1358ca2e67..00000000000 --- a/ui/src/ui/views/overview-quick-actions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { html } from "lit"; -import { t } from "../../i18n/index.ts"; -import { icons } from "../icons.ts"; - -export type OverviewQuickActionsProps = { - onNavigate: (tab: string) => void; - onRefresh: () => void; -}; - -export function renderOverviewQuickActions(props: OverviewQuickActionsProps) { - return html` -
- - - - -
- `; -} diff --git a/ui/src/ui/views/overview.ts b/ui/src/ui/views/overview.ts index 3342b340759..b18c2ce2248 100644 --- a/ui/src/ui/views/overview.ts +++ b/ui/src/ui/views/overview.ts @@ -1,23 +1,10 @@ -import { html, nothing } from "lit"; +import { html } from "lit"; import { t, i18n, type Locale } from "../../i18n/index.ts"; -import type { EventLogEntry } from "../app-events.ts"; import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts"; import type { GatewayHelloOk } from "../gateway.ts"; -import { icons } from "../icons.ts"; +import { formatNextRun } from "../presenter.ts"; import type { UiSettings } from "../storage.ts"; -import type { - AttentionItem, - CronJob, - CronStatus, - SessionsListResult, - SessionsUsageResult, - SkillStatusReport, -} from "../types.ts"; -import { renderOverviewAttention } from "./overview-attention.ts"; -import { renderOverviewCards } from "./overview-cards.ts"; -import { renderOverviewEventLog } from "./overview-event-log.ts"; import { shouldShowPairingHint } from "./overview-hints.ts"; -import { renderOverviewLogTail } from "./overview-log-tail.ts"; export type OverviewProps = { connected: boolean; @@ -30,24 +17,11 @@ export type OverviewProps = { cronEnabled: boolean | null; cronNext: number | null; lastChannelsRefresh: number | null; - // New dashboard data - usageResult: SessionsUsageResult | null; - sessionsResult: SessionsListResult | null; - skillsReport: SkillStatusReport | null; - cronJobs: CronJob[]; - cronStatus: CronStatus | null; - attentionItems: AttentionItem[]; - eventLog: EventLogEntry[]; - overviewLogLines: string[]; - streamMode: boolean; onSettingsChange: (next: UiSettings) => void; onPasswordChange: (next: string) => void; onSessionKeyChange: (next: string) => void; onConnect: () => void; onRefresh: () => void; - onNavigate: (tab: string) => void; - onRefreshLogs: () => void; - onToggleStreamMode: () => void; }; export function renderOverview(props: OverviewProps) { @@ -60,7 +34,7 @@ export function renderOverview(props: OverviewProps) { | undefined; const uptime = snapshot?.uptimeMs ? formatDurationHuman(snapshot.uptimeMs) : t("common.na"); const tick = snapshot?.policy?.tickIntervalMs - ? `${(snapshot.policy.tickIntervalMs / 1000).toFixed(snapshot.policy.tickIntervalMs % 1000 === 0 ? 0 : 1)}s` + ? `${snapshot.policy.tickIntervalMs}ms` : t("common.na"); const authMode = snapshot?.authMode; const isTrustedProxy = authMode === "trusted-proxy"; @@ -190,7 +164,7 @@ export function renderOverview(props: OverviewProps) {
${t("overview.access.title")}
${t("overview.access.subtitle")}
-
+
- ${ - !props.connected - ? html` -
-
${t("overview.connection.title")}
-
    -
  1. ${t("overview.connection.step1")} -
    openclaw gateway run
    -
  2. -
  3. ${t("overview.connection.step2")} -
    openclaw dashboard --no-open
    -
  4. -
  5. ${t("overview.connection.step3")}
  6. -
  7. ${t("overview.connection.step4")} -
    openclaw doctor --generate-gateway-token
    -
  8. -
-
- ${t("overview.connection.docsHint")} - ${t("overview.connection.docsLink")} -
-
- ` - : nothing - }
@@ -341,43 +283,45 @@ export function renderOverview(props: OverviewProps) {
- ${ - props.streamMode - ? html`
- ${icons.radio} - ${t("overview.streamMode.active")} - -
` - : nothing - } - - ${renderOverviewCards({ - usageResult: props.usageResult, - sessionsResult: props.sessionsResult, - skillsReport: props.skillsReport, - cronJobs: props.cronJobs, - cronStatus: props.cronStatus, - presenceCount: props.presenceCount, - redacted: props.streamMode, - onNavigate: props.onNavigate, - })} - - ${renderOverviewAttention({ items: props.attentionItems })} - -
- ${renderOverviewEventLog({ - events: props.eventLog, - redacted: props.streamMode, - })} - - ${renderOverviewLogTail({ - lines: props.overviewLogLines, - redacted: props.streamMode, - onRefreshLogs: props.onRefreshLogs, - })} -
+
+
+
${t("overview.stats.instances")}
+
${props.presenceCount}
+
${t("overview.stats.instancesHint")}
+
+
+
${t("overview.stats.sessions")}
+
${props.sessionsCount ?? t("common.na")}
+
${t("overview.stats.sessionsHint")}
+
+
+
${t("overview.stats.cron")}
+
+ ${props.cronEnabled == null ? t("common.na") : props.cronEnabled ? t("common.enabled") : t("common.disabled")} +
+
${t("overview.stats.cronNext", { time: formatNextRun(props.cronNext) })}
+
+
+
+
${t("overview.notes.title")}
+
${t("overview.notes.subtitle")}
+
+
+
${t("overview.notes.tailscaleTitle")}
+
+ ${t("overview.notes.tailscaleText")} +
+
+
+
${t("overview.notes.sessionTitle")}
+
${t("overview.notes.sessionText")}
+
+
+
${t("overview.notes.cronTitle")}
+
${t("overview.notes.cronText")}
+
+
+
`; } diff --git a/ui/src/ui/views/usage-styles/usageStyles-part1.ts b/ui/src/ui/views/usage-styles/usageStyles-part1.ts index a6f595170a6..1df314e46b5 100644 --- a/ui/src/ui/views/usage-styles/usageStyles-part1.ts +++ b/ui/src/ui/views/usage-styles/usageStyles-part1.ts @@ -54,16 +54,16 @@ export const usageStylesPart1 = ` align-items: center; gap: 6px; padding: 4px 10px; - background: color-mix(in srgb, var(--accent) 10%, transparent); + background: rgba(255, 77, 77, 0.1); border-radius: 4px; font-size: 12px; - color: var(--accent); + color: #ff4d4d; } .usage-refresh-indicator::before { content: ""; width: 10px; height: 10px; - border: 2px solid var(--accent); + border: 2px solid #ff4d4d; border-top-color: transparent; border-radius: 50%; animation: usage-spin 0.6s linear infinite; @@ -161,36 +161,36 @@ export const usageStylesPart1 = ` border-color: var(--border-strong); } .usage-primary-btn { - background: var(--accent); + background: #ff4d4d; color: #fff; - border-color: var(--accent); + border-color: #ff4d4d; box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.12); } .btn.usage-primary-btn { - background: var(--accent) !important; - border-color: var(--accent) !important; + background: #ff4d4d !important; + border-color: #ff4d4d !important; color: #fff !important; } .usage-primary-btn:hover { - background: var(--accent-strong); - border-color: var(--accent-strong); + background: #e64545; + border-color: #e64545; } .btn.usage-primary-btn:hover { - background: var(--accent-strong) !important; - border-color: var(--accent-strong) !important; + background: #e64545 !important; + border-color: #e64545 !important; } .usage-primary-btn:disabled { - background: color-mix(in srgb, var(--accent) 18%, transparent); - border-color: color-mix(in srgb, var(--accent) 30%, transparent); - color: var(--accent); + background: rgba(255, 77, 77, 0.18); + border-color: rgba(255, 77, 77, 0.3); + color: #ff4d4d; box-shadow: none; cursor: default; opacity: 1; } .usage-primary-btn[disabled] { - background: color-mix(in srgb, var(--accent) 18%, transparent) !important; - border-color: color-mix(in srgb, var(--accent) 30%, transparent) !important; - color: var(--accent) !important; + background: rgba(255, 77, 77, 0.18) !important; + border-color: rgba(255, 77, 77, 0.3) !important; + color: #ff4d4d !important; opacity: 1 !important; } .usage-secondary-btn { @@ -533,8 +533,8 @@ export const usageStylesPart1 = ` border-radius: 8px; padding: 10px; color: var(--text); - background: color-mix(in srgb, var(--accent) 8%, transparent); - border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); + background: rgba(255, 77, 77, 0.08); + border: 1px solid rgba(255, 77, 77, 0.2); display: flex; flex-direction: column; gap: 4px; @@ -554,14 +554,14 @@ export const usageStylesPart1 = ` .usage-hour-cell { height: 28px; border-radius: 6px; - background: color-mix(in srgb, var(--accent) 10%, transparent); - border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); + background: rgba(255, 77, 77, 0.1); + border: 1px solid rgba(255, 77, 77, 0.2); cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; } .usage-hour-cell.selected { - border-color: color-mix(in srgb, var(--accent) 80%, transparent); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); + border-color: rgba(255, 77, 77, 0.8); + box-shadow: 0 0 0 2px rgba(255, 77, 77, 0.2); } .usage-hour-labels { display: grid; @@ -584,8 +584,8 @@ export const usageStylesPart1 = ` width: 14px; height: 10px; border-radius: 4px; - background: color-mix(in srgb, var(--accent) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); + background: rgba(255, 77, 77, 0.15); + border: 1px solid rgba(255, 77, 77, 0.2); } .usage-calendar-labels { display: grid; @@ -603,8 +603,8 @@ export const usageStylesPart1 = ` .usage-calendar-cell { height: 18px; border-radius: 4px; - border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); - background: color-mix(in srgb, var(--accent) 8%, transparent); + border: 1px solid rgba(255, 77, 77, 0.2); + background: rgba(255, 77, 77, 0.08); } .usage-calendar-cell.empty { background: transparent; diff --git a/ui/src/ui/views/usage-styles/usageStyles-part2.ts b/ui/src/ui/views/usage-styles/usageStyles-part2.ts index 98400390d87..75826aec314 100644 --- a/ui/src/ui/views/usage-styles/usageStyles-part2.ts +++ b/ui/src/ui/views/usage-styles/usageStyles-part2.ts @@ -100,7 +100,7 @@ export const usageStylesPart2 = ` color: var(--text); } .chart-toggle .toggle-btn.active { - background: var(--accent); + background: #ff4d4d; color: white; } .chart-toggle.small .toggle-btn { @@ -157,14 +157,14 @@ export const usageStylesPart2 = ` .daily-bar { width: 100%; max-width: var(--bar-max-width, 32px); - background: var(--accent); + background: #ff4d4d; border-radius: 3px 3px 0 0; min-height: 2px; transition: all 0.15s; overflow: hidden; } .daily-bar-wrapper:hover .daily-bar { - background: var(--accent-strong); + background: #cc3d3d; } .daily-bar-label { position: absolute; @@ -282,7 +282,7 @@ export const usageStylesPart2 = ` background: #06b6d4; } .legend-dot.system { - background: var(--accent); + background: #ff4d4d; } .legend-dot.skills { background: #8b5cf6; @@ -360,7 +360,7 @@ export const usageStylesPart2 = ` } .session-bar-fill { height: 100%; - background: color-mix(in srgb, var(--accent) 70%, transparent); + background: rgba(255, 77, 77, 0.7); border-radius: 4px; transition: width 0.3s ease; } @@ -431,27 +431,27 @@ export const usageStylesPart2 = ` fill: var(--muted); } .timeseries-svg .ts-area { - fill: var(--accent); + fill: #ff4d4d; fill-opacity: 0.1; } .timeseries-svg .ts-line { fill: none; - stroke: var(--accent); + stroke: #ff4d4d; stroke-width: 2; } .timeseries-svg .ts-dot { - fill: var(--accent); + fill: #ff4d4d; transition: r 0.15s, fill 0.15s; } .timeseries-svg .ts-dot:hover { r: 5; } .timeseries-svg .ts-bar { - fill: var(--accent); + fill: #ff4d4d; transition: fill 0.15s; } .timeseries-svg .ts-bar:hover { - fill: var(--accent-strong); + fill: #cc3d3d; } .timeseries-svg .ts-bar.output { fill: #ef4444; } .timeseries-svg .ts-bar.input { fill: #f59e0b; } @@ -582,7 +582,7 @@ export const usageStylesPart2 = ` transition: width 0.3s ease; } .context-segment.system { - background: var(--accent); + background: #ff4d4d; } .context-segment.skills { background: #8b5cf6; diff --git a/ui/src/ui/views/usage-styles/usageStyles-part3.ts b/ui/src/ui/views/usage-styles/usageStyles-part3.ts index e78cfa63e23..8a114ab69fd 100644 --- a/ui/src/ui/views/usage-styles/usageStyles-part3.ts +++ b/ui/src/ui/views/usage-styles/usageStyles-part3.ts @@ -121,7 +121,7 @@ export const usageStylesPart3 = ` .sessions-card .session-bar-row.selected { border-color: var(--accent); background: var(--accent-subtle); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 15%, transparent); + box-shadow: inset 0 0 0 1px rgba(255, 77, 77, 0.15); } .sessions-card .session-bar-label { flex: 1 1 auto; @@ -139,7 +139,7 @@ export const usageStylesPart3 = ` opacity: 0.5; } .sessions-card .session-bar-fill { - background: color-mix(in srgb, var(--accent) 55%, transparent); + background: rgba(255, 77, 77, 0.55); } .sessions-clear-btn { margin-left: auto; diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 988b439fde3..161cb9dae3b 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -34,7 +34,7 @@ export default defineConfig(() => { }, server: { host: true, - port: 5174, + port: 5173, strictPort: true, }, };