From 26ab93f0ebfe525c3e5c3dc179f02f9a9efd0c43 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sun, 22 Feb 2026 12:57:44 -0600 Subject: [PATCH] revert(ui): remove recent UI dashboard/theme commits from main Co-authored-by: Cursor --- ui/CHECKLIST.md | 144 --- ui/src/i18n/locales/en.ts | 27 +- ui/src/i18n/locales/pt-BR.ts | 27 +- ui/src/i18n/locales/zh-CN.ts | 27 +- ui/src/i18n/locales/zh-TW.ts | 27 +- ui/src/styles/base.css | 168 ++-- ui/src/styles/chat/agent-chat.css | 159 +++- ui/src/styles/chat/grouped.css | 9 +- ui/src/styles/chat/layout.css | 102 +-- ui/src/styles/chat/sidebar.css | 38 +- ui/src/styles/components.css | 852 +++++------------- ui/src/styles/layout.css | 365 +++----- ui/src/styles/layout.mobile.css | 163 ++-- ui/src/ui/app-render.helpers.node.test.ts | 425 +++++---- ui/src/ui/app-render.helpers.ts | 144 +-- ui/src/ui/app-render.ts | 188 ++-- ui/src/ui/app-scroll.test.ts | 206 +++-- ui/src/ui/app-settings.test.ts | 1 - ui/src/ui/app-settings.ts | 2 +- ui/src/ui/app-view-state.ts | 7 +- ui/src/ui/app.ts | 13 +- ui/src/ui/chat/grouped-render.ts | 46 +- ui/src/ui/components/dashboard-header.ts | 2 +- ui/src/ui/format.test.ts | 127 ++- ui/src/ui/icons.ts | 40 - ui/src/ui/markdown.ts | 2 +- ui/src/ui/navigation.test.ts | 188 ++-- ui/src/ui/storage.ts | 6 - ui/src/ui/theme.ts | 3 +- ui/src/ui/views/agents-panels-overview.ts | 51 +- ui/src/ui/views/agents-panels-status-files.ts | 4 +- ui/src/ui/views/agents-utils.ts | 30 +- ui/src/ui/views/agents.ts | 217 +++-- ui/src/ui/views/chat.test.ts | 166 ++-- ui/src/ui/views/chat.ts | 195 +++- ui/src/ui/views/overview-cards.ts | 140 ++- ui/src/ui/views/overview-log-tail.ts | 17 +- ui/src/ui/views/overview.ts | 4 - ui/src/ui/views/sessions.test.ts | 11 - ui/src/ui/views/sessions.ts | 341 ++----- ui/src/ui/views/skills.ts | 18 +- ui/src/ui/views/usage-render-overview.ts | 4 +- .../views/usage-styles/usageStyles-part1.ts | 14 + .../views/usage-styles/usageStyles-part2.ts | 28 +- .../views/usage-styles/usageStyles-part3.ts | 22 +- ui/src/ui/views/usage.ts | 5 + 46 files changed, 2074 insertions(+), 2701 deletions(-) delete mode 100644 ui/CHECKLIST.md diff --git a/ui/CHECKLIST.md b/ui/CHECKLIST.md deleted file mode 100644 index 7f765fd587b..00000000000 --- a/ui/CHECKLIST.md +++ /dev/null @@ -1,144 +0,0 @@ -# UI Dashboard — Verification Checklist - -Run through this checklist after every change that touches `ui/` files. -Open the dashboard at `http://localhost:` (or the gateway's configured UI URL). - -## Login & Shell - -- [ ] Login gate renders when not authenticated -- [ ] Login with valid password grants access -- [ ] Login with invalid password shows error -- [ ] App shell loads: sidebar, header, content area visible -- [ ] Sidebar shows all tab groups: Chat, Control, Agent, Settings -- [ ] Sidebar collapse/expand works; favicon logo shows when collapsed -- [ ] Router: clicking each sidebar tab navigates and updates URL -- [ ] Browser back/forward navigates between tabs -- [ ] Direct URL navigation (e.g. `/chat`, `/overview`) loads correct tab - -## Themes - -- [ ] Theme switcher cycles through all 5 themes: - - [ ] Dark (Obsidian) - - [ ] Light - - [ ] OpenKnot (Aurora) - - [ ] Field Manual - - [ ] OpenClaw (Chrome) -- [ ] Glass components (cards, panels, inputs) render correctly per theme -- [ ] Theme persists across page reload - -## Overview - -- [ ] Overview tab loads without errors -- [ ] Stat cards render: cost, sessions, skills, cron -- [ ] Cards show accent color borders per kind -- [ ] Cards show hover lift + shadow effect -- [ ] Cards are clickable and navigate to corresponding tab -- [ ] Responsive grid: 4 columns → 2 → 1 at breakpoints -- [ ] Attention items render with correct severity icons/colors (error, warning, info) -- [ ] Event log renders with timestamps -- [ ] Log tail section renders live gateway log lines -- [ ] Quick actions section renders -- [ ] Redact toggle in topbar redacts/reveals sensitive values in cards - -## Chat - -- [ ] Chat view renders message history -- [ ] Sending a message works and response streams in -- [ ] Markdown rendering works in responses (code blocks, lists, links) -- [ ] Tool call cards render collapsed by default -- [ ] Tool cards expand/collapse on click; summary shows tool name/count -- [ ] JSON messages render collapsed by default -- [ ] Delete message: trash icon appears on hover, click removes message group -- [ ] Deleted messages persist across reload (localStorage) -- [ ] Clear history button resets session via `sessions.reset` RPC -- [ ] Agent selector dropdown appears when multiple agents configured -- [ ] Switching agents updates session key and reloads history -- [ ] Session list panel: shows all sessions for current agent -- [ ] Session list: clicking a session switches to it -- [ ] Input history (up/down arrow) recalls previous messages -- [ ] Slash command menu opens on `/` keystroke -- [ ] Slash commands show icons, categories, and grouping -- [ ] Pinned messages render if present - -## Command Palette - -- [ ] Opens via keyboard shortcut or UI button -- [ ] Fuzzy search filters commands as you type -- [ ] Results grouped by category with labels -- [ ] Selecting a command executes it -- [ ] "No results" message when nothing matches -- [ ] Clicking overlay closes palette -- [ ] Escape key closes palette - -## Agents - -- [ ] Agent tab loads agent list -- [ ] Agent overview panel: identity card with name, ID, avatar color -- [ ] Agent config display: model, tools, skills shown -- [ ] Agent panels: overview, status/files, tools/skills tabs work -- [ ] Tab counts show for files, skills, channels, cron -- [ ] Sidebar agent filter input filters agents in multi-agent setup -- [ ] Agent actions menu: "copy ID" and "set as default" work -- [ ] Chip-based fallback input (model selection): Enter/comma adds chips - -## Channels & Instances - -- [ ] Channels tab lists connected channels -- [ ] Instances tab lists connected instances -- [ ] Host/IP blurred by default in Connected Instances -- [ ] Reveal toggle shows actual host/IP values -- [ ] Nostr profile form renders if nostr channel present - -## Privacy & Redaction - -- [ ] Topbar redact toggle visible; default is stream mode on -- [ ] Redact ON: sensitive values masked in overview cards -- [ ] Redact ON: cost digits blurred -- [ ] Redact ON: access card blurred -- [ ] Redact ON: raw config JSON masks sensitive values with count badge -- [ ] Redact OFF: all values visible - -## Config - -- [ ] Config tab renders current gateway configuration -- [ ] Config form fields editable -- [ ] Sensitive config values masked when redact is on -- [ ] Config analysis view loads - -## Other Tabs - -- [ ] Sessions tab loads session list -- [ ] Usage tab loads usage statistics with styled sections -- [ ] Cron tab lists cron jobs with status -- [ ] Skills tab lists skills with status report -- [ ] Nodes tab loads -- [ ] Debug tab renders debug info -- [ ] Logs tab renders - -## i18n - -- [ ] English locale loads by default -- [ ] All visible strings use i18n keys (no hardcoded English in templates) -- [ ] zh-CN locale keys present -- [ ] zh-TW locale keys present -- [ ] pt-BR locale keys present - -## Responsive & Mobile - -- [ ] Sidebar collapses on narrow viewport -- [ ] Bottom tabs render on mobile breakpoint -- [ ] Card grid reflows: 4 → 2 → 1 columns -- [ ] Chat input usable on mobile -- [ ] No horizontal overflow on any tab at 375px width - -## Build & Tests - -- [ ] `pnpm build` completes without errors -- [ ] `pnpm test` passes — specifically `ui/` test files: - - [ ] `app-gateway.node.test.ts` - - [ ] `app-settings.test.ts` - - [ ] `config-form.browser.test.ts` - - [ ] `config.browser.test.ts` - - [ ] `chat.test.ts` -- [ ] No new TypeScript errors: `pnpm tsgo` -- [ ] No lint/format issues: `pnpm check` diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 71210283f4e..5c6cdcff7b3 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -21,7 +21,6 @@ export const en: TranslationMap = { settings: "Settings", expand: "Expand sidebar", collapse: "Collapse sidebar", - resize: "Resize sidebar", }, tabs: { agents: "Agents", @@ -39,19 +38,19 @@ export const en: TranslationMap = { logs: "Logs", }, subtitles: { - agents: "Workspaces, tools, identities.", - overview: "Status, entry points, health.", - channels: "Channels and settings.", - instances: "Connected clients and nodes.", - sessions: "Active sessions and defaults.", - usage: "API usage and costs.", - cron: "Wakeups and recurring runs.", - skills: "Skills and API keys.", - nodes: "Paired devices and commands.", - chat: "Gateway chat for quick interventions.", - config: "Edit openclaw.json.", - debug: "Snapshots, events, RPC.", - logs: "Live gateway logs.", + agents: "Manage agent workspaces, tools, and identities.", + overview: "Gateway status, entry points, and a fast health read.", + channels: "Manage channels and settings.", + instances: "Presence beacons from connected clients and nodes.", + sessions: "Inspect active sessions and adjust per-session defaults.", + usage: "Monitor API usage and costs.", + cron: "Schedule wakeups and recurring agent runs.", + skills: "Manage skill availability and API key injection.", + nodes: "Paired devices, capabilities, and command exposure.", + chat: "Direct gateway chat session for quick interventions.", + config: "Edit ~/.openclaw/openclaw.json safely.", + debug: "Gateway snapshots, events, and manual RPC calls.", + logs: "Live tail of the gateway file logs.", }, overview: { access: { diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index c13b9786a06..9d848e2a183 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -21,7 +21,6 @@ export const pt_BR: TranslationMap = { settings: "Configurações", expand: "Expandir barra lateral", collapse: "Recolher barra lateral", - resize: "Redimensionar barra lateral", }, tabs: { agents: "Agentes", @@ -39,19 +38,19 @@ export const pt_BR: TranslationMap = { logs: "Logs", }, subtitles: { - agents: "Espaços, ferramentas, identidades.", - overview: "Status, entrada, saúde.", - channels: "Canais e configurações.", - instances: "Clientes e nós conectados.", - sessions: "Sessões ativas e padrões.", - usage: "Uso e custos da API.", - cron: "Despertares e execuções.", - skills: "Habilidades e chaves API.", - nodes: "Dispositivos e comandos.", - chat: "Chat do gateway para intervenções rápidas.", - config: "Editar openclaw.json.", - debug: "Snapshots, eventos, RPC.", - logs: "Logs ao vivo do gateway.", + agents: "Gerenciar espaços de trabalho, ferramentas e identidades de agentes.", + overview: "Status do gateway, pontos de entrada e leitura rápida de saúde.", + channels: "Gerenciar canais e configurações.", + instances: "Beacons de presença de clientes e nós conectados.", + sessions: "Inspecionar sessões ativas e ajustar padrões por sessão.", + usage: "Monitorar uso e custos da API.", + cron: "Agendar despertares e execuções recorrentes de agentes.", + skills: "Gerenciar disponibilidade de habilidades e injeção de chaves de API.", + nodes: "Dispositivos pareados, capacidades e exposição de comandos.", + chat: "Sessão de chat direta com o gateway para intervenções rápidas.", + config: "Editar ~/.openclaw/openclaw.json com segurança.", + debug: "Snapshots do gateway, eventos e chamadas RPC manuais.", + logs: "Acompanhamento ao vivo dos logs de arquivo do gateway.", }, overview: { access: { diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index df2a9503db8..566a9cd0d4c 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -21,7 +21,6 @@ export const zh_CN: TranslationMap = { settings: "设置", expand: "展开侧边栏", collapse: "折叠侧边栏", - resize: "调整侧边栏大小", }, tabs: { agents: "代理", @@ -39,19 +38,19 @@ export const zh_CN: TranslationMap = { logs: "日志", }, subtitles: { - agents: "工作区、工具、身份。", - overview: "状态、入口点、健康。", - channels: "频道和设置。", - instances: "已连接客户端和节点。", - sessions: "活动会话和默认设置。", - usage: "API 使用情况和成本。", - cron: "唤醒和重复运行。", - skills: "技能和 API 密钥。", - nodes: "配对设备和命令。", - chat: "网关聊天,快速干预。", - config: "编辑 openclaw.json。", - debug: "快照、事件、RPC。", - logs: "实时网关日志。", + agents: "管理代理工作区、工具和身份。", + overview: "网关状态、入口点和快速健康读取。", + channels: "管理频道和设置。", + instances: "来自已连接客户端和节点的在线信号。", + sessions: "检查活动会话并调整每个会话的默认设置。", + usage: "监控 API 使用情况和成本。", + cron: "安排唤醒和重复的代理运行。", + skills: "管理技能可用性和 API 密钥注入。", + nodes: "配对设备、功能和命令公开。", + chat: "用于快速干预的直接网关聊天会话。", + config: "安全地编辑 ~/.openclaw/openclaw.json。", + debug: "网关快照、事件和手动 RPC 调用。", + logs: "网关文件日志的实时追踪。", }, overview: { access: { diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index b3591f19cf3..5fa32aa31c1 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -21,7 +21,6 @@ export const zh_TW: TranslationMap = { settings: "設置", expand: "展開側邊欄", collapse: "折疊側邊欄", - resize: "調整側邊欄大小", }, tabs: { agents: "代理", @@ -39,19 +38,19 @@ export const zh_TW: TranslationMap = { logs: "日誌", }, subtitles: { - agents: "工作區、工具、身份。", - overview: "狀態、入口點、健康。", - channels: "頻道和設置。", - instances: "已連接客戶端和節點。", - sessions: "活動會話和默認設置。", - usage: "API 使用情況和成本。", - cron: "喚醒和重複運行。", - skills: "技能和 API 密鑰。", - nodes: "配對設備和命令。", - chat: "網關聊天,快速干預。", - config: "編輯 openclaw.json。", - debug: "快照、事件、RPC。", - logs: "實時網關日誌。", + agents: "管理代理工作區、工具和身份。", + overview: "網關狀態、入口點和快速健康讀取。", + channels: "管理頻道和設置。", + instances: "來自已連接客戶端和節點的在線信號。", + sessions: "檢查活動會話並調整每個會話的默認設置。", + usage: "監控 API 使用情況和成本。", + cron: "安排喚醒和重複的代理運行。", + skills: "管理技能可用性和 API 密鑰注入。", + nodes: "配對設備、功能和命令公開。", + chat: "用於快速干預的直接網關聊天會話。", + config: "安全地編輯 ~/.openclaw/openclaw.json。", + debug: "網關快照、事件和手動 RPC 調用。", + logs: "網關文件日志的實時追蹤。", }, overview: { access: { diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index 165a2a31478..01f9fb3e641 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -96,55 +96,55 @@ --radius-full: 9999px; } -/* ─── Theme: light — Luxe Cream & Coral ─── */ +/* ─── Theme: light (Docs) — Warm Editorial Dark ─── */ :root[data-theme="light"] { - color-scheme: light; + color-scheme: dark; - --vscode-bg: #faf7f2; - --vscode-sidebar: #f5f0e8; - --vscode-panel: #fffef9; - --vscode-panel-border: rgba(26, 22, 20, 0.08); - --vscode-surface: #fffef9; - --vscode-hover: #f0ebe3; - --vscode-contrast: #f0ebe3; - --vscode-text: #1a1614; - --vscode-muted: #6b5d54; - --vscode-subtle: #9c8f84; - --vscode-ghost: #ebe6df; - --vscode-accent: #c73526; - --vscode-accent-alpha: rgba(199, 53, 38, 0.12); - --vscode-selection: rgba(199, 53, 38, 0.18); - --vscode-success: #0d9b7a; - --vscode-danger: #c73526; + --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: #c73526; - --kn-claw-bright: #e85a4a; - --kn-claw-dim: rgba(199, 53, 38, 0.14); - --kn-claw-ember: #d94a3a; - --kn-claw-deep: #9a2a1e; - --kn-ocean: #faf7f2; - --kn-ocean-bright: #fffef9; - --kn-ocean-mid: #f5f0e8; - --kn-ocean-dim: rgba(250, 247, 242, 0.9); - --kn-ocean-deep: #f0ebe3; - --kn-silver: #6b5d54; - --kn-silver-bright: #1a1614; - --kn-silver-dim: rgba(107, 93, 84, 0.12); - --kn-bioluminescence: #0d9b7a; - --kn-warm-dark: #1a1614; - --kn-void: #ebe6df; + --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: 12px; - --glass-saturate: 110%; - --glass-bg: rgba(255, 254, 249, 0.88); - --glass-bg-elevated: rgba(255, 255, 255, 0.95); - --glass-border: rgba(26, 22, 20, 0.1); - --glass-border-hover: rgba(199, 53, 38, 0.35); - --glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.9); - --glass-shadow-sm: 0 2px 12px rgba(26, 22, 20, 0.06), 0 1px 3px rgba(26, 22, 20, 0.04); - --glass-shadow-md: 0 8px 32px rgba(26, 22, 20, 0.08), 0 2px 8px rgba(26, 22, 20, 0.04); - --glass-shadow-lg: 0 20px 56px rgba(26, 22, 20, 0.12), 0 4px 16px rgba(26, 22, 20, 0.06); + --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; @@ -270,6 +270,64 @@ --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"] { @@ -337,6 +395,7 @@ :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); @@ -491,16 +550,6 @@ --agent-tab-hover-bg: var(--vscode-accent-alpha); } -/* Light theme semantic overrides (accent buttons need dark text) */ -:root[data-theme="light"] { - --card-highlight: rgba(255, 255, 255, 0.85); - --accent-foreground: #ffffff; - --primary-foreground: #ffffff; - --destructive-foreground: #ffffff; - --focus-offset-color: var(--bg); - --grid-line: rgba(26, 22, 20, 0.06); -} - /* ─── Accessibility: High Contrast ─── */ @media (prefers-contrast: more) { @@ -724,17 +773,16 @@ select { display: none; } -/* ─── light — Luxe Cream ambient gradient ─── */ +/* ─── openai — Crimson atmosphere ─── */ -:root[data-theme="light"] body { +:root[data-theme="openai"] body { background: - radial-gradient(ellipse 90% 60% at 50% -15%, rgba(199, 53, 38, 0.04) 0%, transparent 55%), - radial-gradient(ellipse 70% 50% at 85% 40%, rgba(13, 155, 122, 0.03) 0%, transparent 50%), - radial-gradient(ellipse 60% 40% at 15% 80%, rgba(199, 53, 38, 0.02) 0%, transparent 45%), + 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="light"] body::after { +:root[data-theme="openai"] body::after { display: none; } diff --git a/ui/src/styles/chat/agent-chat.css b/ui/src/styles/chat/agent-chat.css index 1642dd7d192..13d4023a54b 100644 --- a/ui/src/styles/chat/agent-chat.css +++ b/ui/src/styles/chat/agent-chat.css @@ -107,12 +107,9 @@ letter-spacing: 0.01em; } -.agent-chat__badge svg, -.agent-chat__badge img { +.agent-chat__badge svg { width: 14px; height: 14px; - object-fit: contain; - vertical-align: -0.15em; } /* ─── Starter Cards ─── */ @@ -242,17 +239,6 @@ flex-shrink: 0; } -.agent-chat__avatar--logo { - background: transparent; -} - -.agent-chat__avatar--logo svg, -.agent-chat__avatar--logo img { - width: 48px; - height: 48px; - object-fit: contain; -} - .agent-chat__avatar--sm { width: 24px; height: 24px; @@ -1138,51 +1124,164 @@ display: flex; align-items: center; justify-content: space-between; - padding: 2px 8px; - border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent); + padding: 6px 16px; + border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); flex-shrink: 0; - gap: 4px; - min-height: 28px; + gap: 8px; } .chat-agent-bar__left { display: flex; align-items: center; - gap: 8px; + gap: 10px; min-width: 0; } .chat-agent-bar__right { display: flex; align-items: center; - gap: 2px; + gap: 4px; flex-shrink: 0; } .chat-agent-bar__name { - font-size: 11px; + font-size: 13px; font-weight: 600; color: var(--text); } .chat-agent-select { - background: transparent; - border: none; + background: color-mix(in srgb, var(--secondary) 70%, transparent); + border: 1px solid var(--border); + border-radius: var(--radius-md); color: var(--text); - font-size: 11px; - font-weight: 600; - padding: 0 14px 0 0; + 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='8' height='8' 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-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 0 center; + background-position: right 6px center; + transition: + border-color 150ms ease, + background 150ms ease; } .chat-agent-select:hover { - color: var(--accent); + 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 d9267cc192b..46cd18f4e24 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -7,7 +7,7 @@ display: flex; gap: 12px; align-items: flex-start; - margin-bottom: 8px; + margin-bottom: 16px; margin-left: 4px; margin-right: 16px; } @@ -124,13 +124,6 @@ img.chat-avatar { object-position: center; } -/* Logo avatar (OpenClaw favicon) - contain to show full logo */ -img.chat-avatar.chat-avatar--logo { - object-fit: contain; - padding: 6px; - box-sizing: border-box; -} - /* Minimal Bubble Design - dynamic width based on content */ .chat-bubble { position: relative; diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index f99ac1c56b8..6c4123de8d3 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -9,8 +9,7 @@ flex-direction: column; flex: 1 1 0; height: 100%; - min-height: 0; - /* Allow flex shrinking */ + min-height: 0; /* Allow flex shrinking */ overflow: hidden; background: transparent !important; border: none !important; @@ -22,18 +21,18 @@ display: flex; justify-content: space-between; align-items: center; - gap: 8px; + gap: 12px; flex-wrap: nowrap; flex-shrink: 0; - padding-bottom: 4px; - margin-bottom: 4px; + padding-bottom: 12px; + margin-bottom: 12px; background: transparent; } .chat-header__left { display: flex; align-items: center; - gap: 8px; + gap: 12px; flex-wrap: wrap; min-width: 0; } @@ -41,23 +40,21 @@ .chat-header__right { display: flex; align-items: center; - gap: 6px; + gap: 8px; } .chat-session { - min-width: 140px; + min-width: 180px; } /* Chat thread - scrollable middle section, transparent */ .chat-thread { - flex: 1 1 0; - /* Grow, shrink, and use 0 base for proper scrolling */ + flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */ overflow-y: auto; overflow-x: hidden; - padding: 6px 6px 4px; + padding: 14px 8px; margin: 0 -4px; - min-height: 0; - /* Allow shrinking for flex scroll behavior */ + min-height: 0; /* Allow shrinking for flex scroll behavior */ border-radius: var(--radius-lg); background: linear-gradient( 180deg, @@ -154,10 +151,9 @@ flex-shrink: 0; display: flex; flex-direction: column; - gap: 8px; - margin-top: auto; - /* Push to bottom of flex container */ - padding: 6px 6px 6px; + 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); z-index: 10; @@ -174,8 +170,7 @@ border: 1px solid var(--border); width: fit-content; max-width: 100%; - align-self: flex-start; - /* Don't stretch in flex column parent */ + align-self: flex-start; /* Don't stretch in flex column parent */ } .chat-attachment { @@ -323,13 +318,13 @@ display: flex; align-items: center; justify-content: flex-start; - gap: 8px; + gap: 12px; flex-wrap: wrap; } .chat-controls__session { - min-width: 120px; - max-width: 220px; + min-width: 140px; + max-width: 300px; } .chat-controls__thinking { @@ -341,9 +336,9 @@ /* Icon button style */ .btn--icon { - padding: 0 !important; - min-width: 32px; - height: 32px; + padding: 8px !important; + min-width: 36px; + height: 36px; display: inline-flex; align-items: center; justify-content: center; @@ -352,17 +347,12 @@ border-radius: var(--radius-md); } -/* Controls separator — renders as a thin vertical divider */ +/* Controls separator */ .chat-controls__separator { - width: 1px; - height: 32px; - background: var(--border); - font-size: 0; - color: transparent; - overflow: hidden; - align-self: center; - flex-shrink: 0; - margin: 0 4px; + color: var(--border); + font-size: 18px; + margin: 0 8px; + font-weight: 300; } .btn--icon:hover { @@ -370,18 +360,24 @@ border-color: var(--border-strong); } -/* Ensure chat toolbar toggles have a clearly visible active state. */ -.chat-controls .btn--icon.active { - border-color: var(--accent); - background: var(--accent-subtle); - color: var(--accent); +/* 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); } .btn--icon svg { display: block; - width: 16px; - height: 16px; - flex-shrink: 0; + width: 18px; + height: 18px; stroke: currentColor; fill: none; stroke-width: 1.5px; @@ -390,9 +386,9 @@ } .chat-controls__session select { - padding: 0 28px 0 10px; + padding: 6px 10px; font-size: 13px; - max-width: 220px; + max-width: 300px; overflow: hidden; text-overflow: ellipsis; } @@ -402,16 +398,15 @@ align-items: center; gap: 4px; font-size: 12px; - padding: 0 10px; - height: 32px; + padding: 4px 10px; background: color-mix(in srgb, var(--secondary) 90%, transparent); - border-radius: var(--radius-md); + border-radius: var(--radius-sm); border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); } @media (max-width: 640px) { .chat-session { - min-width: 80px; + min-width: 140px; } .chat-compose { @@ -420,15 +415,10 @@ .chat-controls { flex-wrap: wrap; - gap: 3px; + gap: 8px; } .chat-controls__session { - min-width: 80px; - max-width: 160px; - } - - .chat-controls__separator { - display: none; + min-width: 120px; } } diff --git a/ui/src/styles/chat/sidebar.css b/ui/src/styles/chat/sidebar.css index 22704cf848f..bc2949309d5 100644 --- a/ui/src/styles/chat/sidebar.css +++ b/ui/src/styles/chat/sidebar.css @@ -18,8 +18,7 @@ .chat-sidebar { flex: 1; - min-width: 200px; - container-type: inline-size; + min-width: 300px; border-left: 1px solid color-mix(in srgb, var(--border) 90%, transparent); display: flex; flex-direction: column; @@ -78,14 +77,11 @@ flex: 1; overflow: auto; padding: 16px; - min-width: 0; } .sidebar-markdown { font-size: 14px; line-height: 1.6; - overflow-wrap: break-word; - word-break: break-word; } .sidebar-markdown pre { @@ -101,38 +97,6 @@ font-size: 13px; } -/* Minimal state when sidebar is narrow: hide content, show expand hint */ -@container (max-width: 260px) { - .chat-sidebar .sidebar-header { - padding: 6px 8px; - border-bottom: none; - justify-content: flex-end; - } - - .chat-sidebar .sidebar-title { - display: none; - } - - .chat-sidebar .sidebar-content { - display: flex; - align-items: center; - justify-content: center; - padding: 8px; - overflow: hidden; - } - - .chat-sidebar .sidebar-content > * { - display: none; - } - - .chat-sidebar .sidebar-content::before { - content: "← Drag to expand"; - font-size: 11px; - color: var(--muted); - white-space: nowrap; - } -} - /* Mobile: Full-screen modal */ @media (max-width: 768px) { .chat-split-container--open { diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index ea5991edc84..e77a471f64b 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -111,7 +111,7 @@ border: 1px solid var(--border); background: var(--card); border-radius: var(--radius-lg); - padding: 14px 16px; + padding: 20px; animation: rise 0.35s var(--ease-out) backwards; transition: border-color var(--duration-normal) var(--ease-out), @@ -130,7 +130,7 @@ } .card-title { - font-size: 15px; + font-size: 16px; font-weight: 600; letter-spacing: -0.02em; color: var(--text-strong); @@ -138,9 +138,9 @@ .card-sub { color: var(--muted); - font-size: 12px; - margin-top: 4px; - line-height: 1.45; + font-size: 14px; + margin-top: 6px; + line-height: 1.5; } /* =========================================== @@ -150,13 +150,12 @@ .stat { background: color-mix(in srgb, var(--card) 96%, transparent); border-radius: var(--radius-md); - padding: 10px 12px; + padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--border) 92%, transparent); transition: border-color var(--duration-normal) var(--ease-out), box-shadow var(--duration-normal) var(--ease-out); box-shadow: inset 0 1px 0 var(--card-highlight); - text-align: center; } .stat:hover { @@ -315,37 +314,109 @@ } /* =========================================== - Theme Select + Theme Toggle =========================================== */ -.theme-select { - appearance: none; +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; border: 1px solid var(--clay-border-color); - border-radius: var(--radius-md); - padding: 4px 28px 4px 10px; - height: 28px; - min-width: 90px; - font-size: 12px; - font-weight: 500; - color: var(--text); + border-radius: 999px; + padding: 5px; + height: 36px; background: var(--clay-bg); - 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='%236e7a8a' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; + overflow: hidden; + max-width: 36px; + transition: + max-width var(--clay-duration-normal) var(--clay-easing), + padding var(--clay-duration-normal) var(--clay-easing); +} + +@media (hover: hover) { + .theme-toggle:hover { + max-width: 400px; + padding: 4px 6px; + } +} + +.theme-toggle:focus-within { + max-width: 400px; + padding: 4px 6px; +} + +.theme-toggle.theme-toggle--open { + max-width: 400px; + padding: 4px 6px; +} + +.theme-btn { + border: 0; + 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: - border-color var(--clay-duration-fast) var(--clay-easing), - box-shadow 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-select:hover { - border-color: var(--border-strong); +.theme-btn.active { + padding: 6px 8px; + background: var(--clay-bg-button); + color: var(--text); + box-shadow: var(--clay-shadow-pressed); } -.theme-select:focus-visible { - outline: none; - border-color: var(--ring); - box-shadow: var(--focus-ring); +.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; + color: var(--text); +} + +.theme-btn:active { + transform: scale(0.93); +} + +.theme-btn svg { + width: 16px; + height: 16px; + stroke: currentColor; + fill: none; + stroke-width: 1.5px; + stroke-linecap: round; + stroke-linejoin: round; } /* =========================================== @@ -406,15 +477,14 @@ } .btn svg { - display: block; width: 16px; height: 16px; - flex-shrink: 0; stroke: currentColor; fill: none; stroke-width: 1.5px; stroke-linecap: round; stroke-linejoin: round; + flex-shrink: 0; } .btn.primary { @@ -556,47 +626,6 @@ align-items: center; } -.field-inline { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 13px; -} - -.field-inline span { - color: var(--muted); - font-weight: 500; - white-space: nowrap; -} - -.field-inline input:not([type="checkbox"]) { - border: 1px solid color-mix(in srgb, var(--input) 92%, transparent); - background: color-mix(in srgb, var(--card) 96%, var(--bg)); - border-radius: var(--radius-md); - padding: 6px 10px; - font-size: 13px; - outline: none; - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; -} - -.field-inline input:not([type="checkbox"]):focus-visible { - border-color: var(--ring); - box-shadow: var(--focus-ring); - background: var(--card); -} - -.field-inline.checkbox { - cursor: pointer; - user-select: none; -} - -.field-inline.checkbox input[type="checkbox"] { - margin: 0; - accent-color: var(--accent); -} - .config-form .field.checkbox { grid-template-columns: 18px minmax(0, 1fr); column-gap: 10px; @@ -615,6 +644,27 @@ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } +:root[data-theme="light"] .field input, +:root[data-theme="light"] .field textarea, +:root[data-theme="light"] .field select { + background: var(--card); + border-color: var(--input); +} + +:root[data-theme="light"] .btn { + background: var(--bg); + border-color: var(--input); +} + +:root[data-theme="light"] .btn:hover { + background: var(--bg-hover); +} + +:root[data-theme="light"] .btn.primary { + background: var(--accent); + border-color: var(--accent); +} + /* =========================================== Utilities =========================================== */ @@ -1118,12 +1168,6 @@ min-width: 0; } -/* Sessions table: wider key column */ -.data-table th:first-child, -.data-table td:first-child { - min-width: 280px; -} - .session-key-cell .session-link, .session-key-display-name { overflow-wrap: anywhere; @@ -1134,273 +1178,6 @@ font-size: 11px; } -/* =========================================== - Data Table (shadcn-inspired) - =========================================== */ - -.data-table-wrapper { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--card); - overflow: hidden; -} - -.data-table-toolbar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 12px; - padding: 16px; - border-bottom: 1px solid var(--border); -} - -.data-table-search { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - min-width: 200px; - max-width: 320px; -} - -.data-table-search input { - flex: 1; - height: 36px; - padding: 0 12px; - font-size: 14px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg); - color: var(--text); - transition: border-color var(--duration-fast) ease; -} - -.data-table-search input::placeholder { - color: var(--muted); -} - -.data-table-search input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px var(--accent-subtle); -} - -.data-table-search .data-table-search__icon { - width: 16px; - height: 16px; - color: var(--muted); - flex-shrink: 0; -} - -.data-table-container { - overflow-x: auto; -} - -.data-table { - width: 100%; - border-collapse: collapse; - font-size: 13px; -} - -.data-table th, -.data-table td { - padding: 12px 16px; - text-align: left; - border-bottom: 1px solid var(--border); - vertical-align: middle; -} - -.data-table th { - font-weight: 600; - color: var(--muted); - background: color-mix(in srgb, var(--secondary) 50%, transparent); - white-space: nowrap; -} - -.data-table th[data-sortable] { - cursor: pointer; - user-select: none; -} - -.data-table th[data-sortable]:hover { - color: var(--text); -} - -.data-table th .data-table-sort-icon { - display: inline-flex; - margin-left: 4px; - opacity: 0.5; - vertical-align: middle; -} - -.data-table th[data-sort-dir] .data-table-sort-icon { - opacity: 1; -} - -.data-table tbody tr { - transition: background var(--duration-fast) ease; -} - -.data-table tbody tr:hover { - background: var(--bg-hover); -} - -.data-table tbody tr:last-child td { - border-bottom: none; -} - -.data-table-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - font-size: 11px; - font-weight: 500; - border-radius: var(--radius-full); - text-transform: capitalize; -} - -.data-table-badge--direct { - background: color-mix(in srgb, var(--accent) 15%, transparent); - color: var(--accent); -} - -.data-table-badge--group { - background: color-mix(in srgb, var(--ok) 15%, transparent); - color: var(--ok); -} - -.data-table-badge--global { - background: color-mix(in srgb, var(--muted) 30%, transparent); - color: var(--muted); -} - -.data-table-badge--unknown { - background: color-mix(in srgb, var(--warn) 15%, transparent); - color: var(--warn); -} - -.data-table-row-actions { - position: relative; - display: inline-flex; -} - -.data-table-row-actions__trigger { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0; - border: none; - border-radius: var(--radius-md); - background: transparent; - color: var(--muted); - cursor: pointer; - transition: - color var(--duration-fast) ease, - background var(--duration-fast) ease; -} - -.data-table-row-actions__trigger:hover { - color: var(--text); - background: var(--bg-hover); -} - -.data-table-row-actions__trigger svg { - width: 16px; - height: 16px; -} - -.data-table-row-actions__menu { - position: absolute; - top: calc(100% + 4px); - right: 0; - z-index: 50; - min-width: 160px; - padding: 4px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--card); - box-shadow: var(--shadow-lg); -} - -.data-table-row-actions__menu button { - display: block; - width: 100%; - padding: 8px 12px; - font-size: 13px; - text-align: left; - border: none; - border-radius: var(--radius-sm); - background: none; - color: var(--text); - cursor: pointer; - transition: background var(--duration-fast) ease; -} - -.data-table-row-actions__menu button:hover { - background: var(--bg-hover); -} - -.data-table-row-actions__menu button.danger { - color: var(--danger); -} - -.data-table-row-actions__menu button.danger:hover { - background: var(--danger-subtle); -} - -.data-table-pagination { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 12px 16px; - border-top: 1px solid var(--border); - font-size: 13px; - color: var(--muted); -} - -.data-table-pagination__info { - flex: 1; -} - -.data-table-pagination__controls { - display: flex; - align-items: center; - gap: 8px; -} - -.data-table-pagination__controls button { - padding: 6px 12px; - font-size: 13px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--card); - color: var(--text); - cursor: pointer; - transition: - border-color var(--duration-fast) ease, - background var(--duration-fast) ease; -} - -.data-table-pagination__controls button:hover:not(:disabled) { - border-color: var(--border-strong); - background: var(--bg-hover); -} - -.data-table-pagination__controls button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.data-table-overlay { - position: fixed; - inset: 0; - z-index: 40; -} - /* =========================================== Log Stream =========================================== */ @@ -1678,7 +1455,6 @@ 100% { border-color: var(--border); } - 50% { border-color: var(--accent); } @@ -1735,7 +1511,6 @@ opacity: 0.4; transform: translateY(0); } - 40% { opacity: 1; transform: translateY(-3px); @@ -1924,9 +1699,9 @@ .shell--chat .chat-compose { position: sticky; bottom: 0; - /* z-index: 5; */ + z-index: 5; margin-top: 0; - padding-top: 6px; + padding-top: 12px; background: linear-gradient(180deg, transparent 0%, var(--bg) 40%); } @@ -2104,129 +1879,21 @@ .agents-layout { display: grid; - grid-template-columns: 1fr; - gap: 10px; + grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); + gap: 16px; } -.agents-toolbar { +.agents-sidebar { display: grid; - gap: 4px; -} - -.agents-toolbar-row { - display: grid; - gap: 3px; -} - -.agents-toolbar-label { - color: var(--muted); - font-size: 12px; - font-weight: 500; -} - -.agents-control-row { - display: grid; - grid-template-columns: 1fr 2fr; - gap: 4px; - align-items: stretch; -} - -.agents-control-select { - min-width: 0; -} - -.agents-control-select .agents-select { - flex: 1; - min-width: 0; - height: 36px; - font-size: 14px; - box-sizing: border-box; - border: 1px solid color-mix(in srgb, var(--input) 60%, transparent); - background: color-mix(in srgb, var(--card) 55%, transparent); - border-radius: var(--radius-md); - padding: 6px 36px 6px 12px; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; - background-size: 16px; - cursor: pointer; - 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; -} - -.agents-control-select .agents-select:focus-visible { - border-color: var(--ring); - box-shadow: var(--focus-ring); - background: color-mix(in srgb, var(--card) 75%, transparent); -} - -.agents-control-select .agents-select:disabled { - opacity: 0.6; - cursor: not-allowed; - background: color-mix(in srgb, var(--secondary) 80%, transparent); -} - -.agents-control-actions { - display: flex; - gap: 4px; - align-items: stretch; - min-width: 0; -} - -.agents-control-actions .agent-actions-toggle { - width: 36px; - height: 36px; - flex-shrink: 0; - padding: 0; - font-size: 18px; - display: inline-flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - background: color-mix(in srgb, var(--secondary) 55%, transparent); - border-color: color-mix(in srgb, var(--border) 60%, transparent); -} - -.agents-control-actions .agent-actions-toggle:hover { - background: color-mix(in srgb, var(--secondary) 75%, transparent); -} - -.agents-control-actions .agents-refresh-btn { - flex: 1; - min-width: 0; - height: 36px; - font-size: 13px; - padding: 0 12px; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - background: color-mix(in srgb, var(--bg-elevated) 55%, transparent); - border-color: color-mix(in srgb, var(--border) 60%, transparent); -} - -.agents-refresh-btn:hover:not(:disabled) { - background: color-mix(in srgb, var(--bg-hover) 75%, transparent); -} - -.agents-toolbar-field { - min-width: 160px; - max-width: 280px; - gap: 4px; -} - -.agents-select { - width: 100%; + gap: 12px; + align-self: start; + position: sticky; + top: 16px; } .agents-main { display: grid; - gap: 10px; + gap: 16px; } .agent-list { @@ -2269,19 +1936,9 @@ } .agent-avatar--lg { - width: 40px; - height: 40px; - font-size: 18px; -} - -.agent-avatar--logo { - background: transparent; -} - -.agent-avatar--logo .agent-avatar__img { - width: 100%; - height: 100%; - object-fit: contain; + width: 48px; + height: 48px; + font-size: 20px; } .agent-info { @@ -2302,7 +1959,7 @@ .agent-pill { border: 1px solid var(--border); border-radius: var(--radius-full); - padding: 3px 8px; + padding: 4px 10px; font-size: 11px; color: var(--muted); background: var(--secondary); @@ -2318,28 +1975,23 @@ .agent-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; + gap: 16px; align-items: center; - padding: 10px 14px; } .agent-header-main { display: flex; - gap: 10px; + gap: 16px; align-items: center; } .agent-header-meta { display: grid; justify-items: end; - gap: 4px; + gap: 6px; color: var(--muted); } -.agent-header .card-sub { - margin-top: 2px; -} - .agent-tabs { display: flex; gap: 8px; @@ -2349,7 +2001,7 @@ .agent-tab { border: 1px solid var(--border); border-radius: var(--radius-full); - padding: 5px 12px; + padding: 6px 14px; font-size: 12px; font-weight: 600; background: var(--secondary); @@ -2403,41 +2055,6 @@ gap: 12px; } -.agent-model-fields { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 16px; - align-items: start; -} - -.agent-model-fields .field { - min-width: 0; - overflow: hidden; -} - -.agent-model-fields .field select { - width: 100%; - min-width: 0; - box-sizing: border-box; -} - -.agent-model-fields .agent-chip-input { - min-width: 0; - overflow: hidden; -} - -@media (max-width: 640px) { - .agent-model-fields { - grid-template-columns: 1fr; - } -} - -.agent-model-actions { - display: flex; - justify-content: flex-end; - gap: 8px; -} - .agent-model-meta { display: grid; gap: 6px; @@ -2839,17 +2456,6 @@ text-decoration-style: solid; } -/* =========================================== - Overview Section Dividers - =========================================== */ - -.ov-section-divider { - height: 1px; - background: var(--border); - margin: 22px 0; - opacity: 0.5; -} - /* =========================================== Overview Dashboard Cards =========================================== */ @@ -2861,77 +2467,91 @@ margin-top: 18px; } -.ov-card { +.ov-stat-card { --ov-accent: var(--muted); - all: unset; display: grid; - gap: 4px; - padding: 16px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - border-left: 3px solid var(--ov-accent); - background: var(--card); + gap: 0; + padding: 0; + overflow: hidden; + border-top: 2px solid var(--ov-accent); + position: relative; +} + +.ov-stat-card.clickable { cursor: pointer; - box-shadow: - var(--shadow-sm), - inset 0 1px 0 var(--card-highlight); transition: - border-color var(--duration-normal) var(--ease-out), - box-shadow var(--duration-normal) var(--ease-out), - transform var(--duration-normal) var(--ease-out); - animation: rise 0.35s var(--ease-out) backwards; + border-color 0.15s ease, + transform 0.15s ease, + box-shadow 0.15s ease; } -.ov-card:hover { - border-color: var(--border-strong); - border-left-color: var(--ov-accent); - box-shadow: - var(--shadow-md), - inset 0 1px 0 var(--card-highlight); +.ov-stat-card.clickable:hover { + border-color: var(--accent); transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); } -.ov-card:focus-visible { - outline: none; - border-color: var(--ring); - border-left-color: var(--ov-accent); - box-shadow: var(--focus-ring); -} - -.ov-card[data-kind="cost"] { +.ov-stat-card[data-kind="cost"] { --ov-accent: var(--kn-bioluminescence); } -.ov-card[data-kind="sessions"] { +.ov-stat-card[data-kind="sessions"] { --ov-accent: var(--kn-silver); } -.ov-card[data-kind="skills"] { +.ov-stat-card[data-kind="skills"] { --ov-accent: var(--kn-claw-ember); } -.ov-card[data-kind="cron"] { +.ov-stat-card[data-kind="cron"] { --ov-accent: var(--vscode-accent); } -.ov-card__label { +.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; - font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); + margin-bottom: 6px; + font-weight: 600; } -.ov-card__value { - font-size: 24px; +.ov-stat-card__body .stat-value { + font-size: 22px; font-weight: 700; - letter-spacing: -0.025em; - line-height: 1.15; + letter-spacing: -0.02em; + line-height: 1.1; } -.ov-card__hint { +.ov-stat-card__body .muted { font-size: 12px; - color: var(--muted); + margin-top: 6px; line-height: 1.4; } @@ -2944,52 +2564,35 @@ /* Recent sessions */ -.ov-recent { +.ov-recent-sessions { margin-top: 14px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--card); - padding: 14px 16px; - box-shadow: - var(--shadow-sm), - inset 0 1px 0 var(--card-highlight); - animation: rise 0.35s var(--ease-out) backwards; } -.ov-recent__title { - font-size: 14px; - font-weight: 600; - letter-spacing: -0.02em; - color: var(--text-strong); - margin: 0 0 8px; +.ov-session-list { + margin-top: 10px; } -.ov-recent__list { - list-style: none; - margin: 0; - padding: 0; -} - -.ov-recent__row { - display: grid; - grid-template-columns: minmax(0, 2fr) minmax(0, auto) auto; - gap: 12px; +.ov-session-row { + display: flex; align-items: center; - padding: 7px 0; - border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); + 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-recent__row:last-child { +.ov-session-row:last-child { border-bottom: none; padding-bottom: 0; } -.ov-recent__row:first-child { +.ov-session-row:first-child { padding-top: 0; } -.ov-recent__key { +.ov-session-key { + flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; @@ -2997,31 +2600,16 @@ font-weight: 500; } -.ov-recent__key .blur-digits { +.ov-session-key .blur-digits { filter: blur(5px); transition: filter 200ms ease-out; user-select: none; } -.ov-recent__row:hover .blur-digits { +.ov-session-row:hover .blur-digits { filter: none; } -.ov-recent__model { - color: var(--muted); - font-size: 12px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 120px; -} - -.ov-recent__time { - color: var(--muted); - font-size: 12px; - white-space: nowrap; -} - /* =========================================== Attention Center =========================================== */ @@ -3102,9 +2690,9 @@ .ov-expandable-toggle { display: flex; align-items: center; - gap: 6px; + gap: 8px; cursor: pointer; - font-size: 13px; + font-size: 14px; font-weight: 600; list-style: none; padding: 0; @@ -3203,16 +2791,16 @@ } .ov-log-tail-content { - margin-top: 8px; - max-height: 180px; + margin-top: 12px; + max-height: 250px; overflow: auto; font-family: var(--mono); - font-size: 10px; - line-height: 1.45; + font-size: 11px; + line-height: 1.6; white-space: pre-wrap; - word-break: break-word; + word-break: break-all; background: var(--bg-inset, var(--bg)); - padding: 8px; + padding: 12px; border-radius: var(--radius); border: 1px solid var(--border); } @@ -3283,14 +2871,6 @@ .ov-cards { grid-template-columns: repeat(2, 1fr); } - - .ov-recent__row { - grid-template-columns: minmax(0, 1fr) auto; - } - - .ov-recent__model { - display: none; - } } @media (max-width: 480px) { diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 4b1cd7631e0..384d89c9399 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -3,10 +3,10 @@ =========================================== */ .shell { - --shell-pad: 12px; - --shell-gap: 12px; - --shell-nav-width: 220px; - --shell-topbar-height: 52px; + --shell-pad: 16px; + --shell-gap: 16px; + --shell-nav-width: 240px; + --shell-topbar-height: 62px; --shell-focus-duration: 200ms; --shell-focus-ease: var(--ease-out); height: 100vh; @@ -80,8 +80,8 @@ display: flex; justify-content: space-between; align-items: center; - gap: 10px; - padding: 0 16px; + gap: 12px; + padding: 0 20px; height: var(--shell-topbar-height); background: var(--topbar-bg); backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur)); @@ -102,7 +102,7 @@ display: flex; align-items: center; gap: 6px; - font-size: 0.8rem; + font-size: 0.82rem; min-width: 0; } @@ -142,17 +142,17 @@ .topbar-search { display: flex; align-items: center; - gap: 6px; - padding: 5px 10px; - min-width: 160px; - max-width: 280px; + gap: 8px; + padding: 6px 12px; + min-width: 200px; + max-width: 340px; flex: 1; - height: 30px; + 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: 12px; + font-size: 13px; font-family: var(--font-body); cursor: pointer; transition: @@ -220,10 +220,10 @@ .topbar-connection { display: inline-flex; align-items: center; - gap: 5px; - padding: 3px 8px; + gap: 6px; + padding: 4px 10px; border-radius: var(--radius-full); - font-size: 11px; + font-size: 12px; font-weight: 500; color: var(--danger); background: var(--danger-subtle); @@ -262,8 +262,8 @@ display: inline-flex; align-items: center; justify-content: center; - width: 26px; - height: 26px; + width: 28px; + height: 28px; padding: 0; border: 1px solid transparent; border-radius: var(--radius); @@ -278,8 +278,8 @@ } .topbar-redact svg { - width: 12px; - height: 12px; + width: 14px; + height: 14px; } .topbar-redact:hover { @@ -298,29 +298,23 @@ border-color: color-mix(in srgb, var(--warn) 30%, transparent); } -/* Topbar theme select sizing */ +/* Topbar theme toggle sizing */ -.topbar-status .theme-select { - height: 26px; - min-width: 82px; - font-size: 11px; +.topbar-status .theme-toggle { + height: 30px; +} + +.topbar-status .theme-btn svg { + width: 13px; + height: 13px; } /* =========================================== Navigation Sidebar =========================================== */ -.shell-nav { - grid-area: nav; - display: flex; - flex-direction: column; - min-height: 0; - position: relative; -} - .sidebar { - flex: 1; - min-width: 0; + grid-area: nav; display: flex; flex-direction: column; overflow-y: auto; @@ -341,9 +335,13 @@ display: none; } -.shell--chat-focus .sidebar, -.shell--chat-focus .sidebar-resizer { - display: none; +.shell--chat-focus .sidebar { + width: 0; + padding: 0; + border-width: 0; + overflow: hidden; + pointer-events: none; + opacity: 0; } .sidebar--collapsed { @@ -385,21 +383,6 @@ border-left: 0; } -.sidebar--collapsed .nav-item--active::before { - background: - radial-gradient( - ellipse 120% 28px at 50% -2px, - color-mix(in srgb, var(--accent) 38%, transparent) 0%, - color-mix(in srgb, var(--accent) 14%, transparent) 40%, - transparent 100% - ), - radial-gradient( - ellipse 60% 100% at -4px 50%, - color-mix(in srgb, var(--accent) 28%, transparent) 0%, - transparent 70% - ); -} - .sidebar--collapsed .sidebar-footer { display: flex; flex-direction: column; @@ -414,54 +397,24 @@ height: 44px; } -/* Sidebar resizer handle */ -.sidebar-resizer { - position: absolute; - right: 0; - top: 0; - bottom: 0; - width: 6px; - cursor: col-resize; - z-index: 10; - flex-shrink: 0; - /* Hit area extends beyond visible handle for easier grabbing */ - margin-right: -3px; -} - -.sidebar-resizer::before { - content: ""; - position: absolute; - left: 2px; - top: 20%; - bottom: 20%; - width: 2px; - border-radius: 1px; - background: var(--glass-border); - transition: background 0.15s ease; -} - -.sidebar-resizer:hover::before, -.sidebar-resizer:active::before { - background: var(--glass-border-hover); -} - /* Sidebar header (brand + collapse) */ .sidebar-header { display: flex; flex-direction: row; align-items: center; padding: 10px 8px; - gap: 8px; + gap: 0; flex-shrink: 0; min-height: 54px; } .sidebar-brand { - flex: 1; - min-width: 0; + flex: 2; display: flex; align-items: center; gap: 10px; + min-width: 0; + max-height: 28px; padding-left: 10px; @@ -487,19 +440,13 @@ line-height: 1.1; color: var(--text-strong); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; } .sidebar-collapse-btn { - flex: 0 0 32px; - width: 32px; + flex: 1; height: 32px; @media (max-width: 1100px) { - flex: 0 0 28px; - width: 28px; height: 28px; } @@ -636,7 +583,6 @@ border-radius: var(--radius-md); border: 1px solid transparent; background: transparent; - overflow: hidden; color: var(--muted); cursor: pointer; text-decoration: none; @@ -709,33 +655,6 @@ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent); } -.nav-item--active::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - background: radial-gradient( - ellipse 28px 120% at -2px 50%, - color-mix(in srgb, var(--accent) 38%, transparent) 0%, - color-mix(in srgb, var(--accent) 14%, transparent) 40%, - transparent 100% - ); - opacity: 0; - animation: nav-glow-in 0.4s ease-out 0.05s forwards; - pointer-events: none; -} - -@keyframes nav-glow-in { - from { - opacity: 0; - transform: translateX(-6px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - .nav-item--active .nav-item__icon { opacity: 1; color: var(--accent); @@ -749,17 +668,11 @@ margin-top: auto; } -.sidebar-footer__docs-block { - display: flex; - flex-direction: column; - gap: 4px; -} - .sidebar-version { display: flex; align-items: center; - justify-content: flex-start; - padding: 2px 12px 6px; + justify-content: center; + padding: 6px 10px; } .sidebar-version__text { @@ -783,7 +696,7 @@ .content { grid-area: content; - padding: 12px 14px 24px; + padding: 14px 18px 36px; display: block; min-height: 0; overflow-y: auto; @@ -791,13 +704,13 @@ } .content > * + * { - margin-top: 18px; + margin-top: 24px; } .content--chat { display: flex; flex-direction: column; - gap: 2px; + gap: 24px; overflow: hidden; padding-bottom: 0; } @@ -809,12 +722,10 @@ /* Content header */ .content-header { display: flex; - align-items: center; + align-items: flex-end; justify-content: space-between; - gap: 10px; - height: 36px; - min-height: 36px; - padding: 0; + gap: 16px; + padding: 4px 0; overflow: hidden; transform-origin: top center; transition: @@ -822,37 +733,36 @@ transform var(--shell-focus-duration) var(--shell-focus-ease), max-height var(--shell-focus-duration) var(--shell-focus-ease), padding var(--shell-focus-duration) var(--shell-focus-ease); - max-height: 36px; + max-height: 80px; } .shell--chat-focus .content-header { opacity: 0; transform: translateY(-8px); - max-height: 0; + max-height: 0px; padding: 0; pointer-events: none; } .page-title { - font-size: 18px; - font-weight: 600; - letter-spacing: -0.03em; - line-height: 1.25; + font-size: 28px; + font-weight: 700; + letter-spacing: -0.035em; + line-height: 1.15; color: var(--text-strong); } .page-sub { color: var(--muted); - font-size: 12px; + font-size: 15px; font-weight: 400; - margin-top: 1px; + margin-top: 6px; letter-spacing: -0.01em; } .page-meta { display: flex; - gap: 6px; - align-items: center; + gap: 8px; } /* Chat view header adjustments */ @@ -860,13 +770,10 @@ flex-direction: row; align-items: center; justify-content: space-between; - gap: 10px; + gap: 16px; } .content--chat .content-header > div:first-child { - display: flex; - flex-direction: column; - justify-content: center; text-align: left; } @@ -876,66 +783,6 @@ .content--chat .chat-controls { flex-shrink: 0; - align-items: center; - align-content: center; -} - -/* Chat controls in header — uniform 32px height across all controls */ -.content-header .btn--icon { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 32px; - height: 32px; - padding: 0 !important; -} - -.content-header .btn--icon svg { - display: block; - width: 16px; - height: 16px; - flex-shrink: 0; -} - -.content-header .chat-controls__session { - display: flex; - align-items: center; - gap: 0; - min-width: 0; -} - -.content-header .chat-controls__session select { - height: 32px; - line-height: 1; - font-size: 13px; - font-weight: 600; - letter-spacing: -0.02em; - padding: 0 28px 0 10px; - background-position: right 8px center; - border-radius: var(--radius-md); - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; -} - -.content-header .chat-controls__separator { - width: 1px; - height: 32px; - background: var(--border); - font-size: 0; - color: transparent; - overflow: hidden; - align-self: center; - flex-shrink: 0; - margin: 0 4px; -} - -.content-header .chat-controls__thinking { - height: 32px; - min-height: 32px; - align-items: center; - padding: 0 10px; - font-size: 12px; } /* =========================================== @@ -944,7 +791,7 @@ .grid { display: grid; - gap: 14px; + gap: 20px; } .grid-cols-2 { @@ -957,32 +804,32 @@ .stat-grid { display: grid; - gap: 10px; - grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } .note-grid { display: grid; - gap: 12px; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } .row { display: flex; - gap: 10px; + gap: 12px; align-items: center; } .stack { display: grid; - gap: 10px; + gap: 12px; grid-template-columns: minmax(0, 1fr); } .filters { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 8px; align-items: center; } @@ -992,14 +839,58 @@ @media (max-width: 1100px) { .shell { - --shell-pad: 10px; - --shell-gap: 10px; - --shell-nav-width: 200px; + --shell-pad: 12px; + --shell-gap: 12px; + grid-template-columns: 1fr; + grid-template-rows: auto auto 1fr; + grid-template-areas: + "topbar" + "nav" + "content"; + } + + .sidebar { + 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; + } + + .nav-group { + grid-auto-flow: column; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + margin-bottom: 0; + } + + .grid-cols-2, + .grid-cols-3 { + grid-template-columns: 1fr; } .topbar { - padding: 10px 12px; - gap: 8px; + position: static; + padding: 12px 14px; + gap: 10px; } .topbar-search__kbd { @@ -1010,30 +901,6 @@ flex-wrap: nowrap; } - .content-header { - height: 36px; - min-height: 36px; - max-height: 36px; - padding: 0; - } - - .page-sub { - display: none; - } - - .content { - padding: 10px 12px 20px; - } - - .content > * + * { - margin-top: 14px; - } - - .grid-cols-2, - .grid-cols-3 { - grid-template-columns: 1fr; - } - .table-head, .table-row { grid-template-columns: 1fr; diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css index 74815420a6f..084373ab82f 100644 --- a/ui/src/styles/layout.mobile.css +++ b/ui/src/styles/layout.mobile.css @@ -2,10 +2,60 @@ Mobile Layout =========================================== */ -/* Tablet: keep side nav vertical, narrow sidebar */ +/* Tablet: Horizontal nav */ @media (max-width: 1100px) { - .shell { - --shell-nav-width: 200px; + .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 { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 4px; + padding: 10px 14px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .sidebar-nav::-webkit-scrollbar { + display: none; + } + + .nav-group { + display: contents; + } + + .nav-group__items { + display: contents; + } + + .nav-group__label { + display: none; + } + + .nav-group--collapsed .nav-group__items { + display: contents; + } + + .nav-item { + padding: 8px 14px; + font-size: 13px; + border-radius: var(--radius-md); + white-space: nowrap; + flex-shrink: 0; } } @@ -14,7 +64,6 @@ .shell { --shell-pad: 8px; --shell-gap: 8px; - --shell-nav-width: 180px; } /* Topbar */ @@ -91,31 +140,14 @@ flex-shrink: 0; } - /* Content — compact header on chat, hide on other tabs */ + /* Content */ .content-header { - height: 64px; - min-height: 64px; - padding: 12px 0; - /* This controls the height of the content header on mobile */ - max-height: 64px; - margin-top: 24px; - } - - .content:not(.content--chat) .content-header { - display: none; - } - - .content--chat .page-title { - font-size: 16px; - } - - .content--chat .page-sub { display: none; } .content { - padding: 4px 6px 12px; - gap: 10px; + padding: 4px 4px 16px; + gap: 12px; } /* Cards */ @@ -180,14 +212,10 @@ } /* Chat */ - .chat-agent-bar { - padding: 2px 6px; - } - .chat-header { flex-direction: column; align-items: stretch; - gap: 6px; + gap: 8px; } .chat-header__left { @@ -205,60 +233,40 @@ } .chat-thread { - margin-top: 6px; - padding: 10px 6px; + margin-top: 8px; + padding: 12px 8px; } .chat-msg { - max-width: 92%; + max-width: 90%; } .chat-bubble { - padding: 6px 10px; + padding: 8px 12px; border-radius: var(--radius-md); } .chat-compose { - gap: 6px; + gap: 8px; } .chat-compose__field textarea { - min-height: 52px; - padding: 6px 10px; + min-height: 60px; + padding: 8px 10px; border-radius: var(--radius-md); font-size: 14px; } - .agent-chat__input { - margin: 0 8px 10px; - } - - .agent-chat__toolbar { - padding: 4px 8px; - } - - .agent-chat__input-btn, - .agent-chat__toolbar .btn-ghost { - width: 28px; - height: 28px; - } - - .agent-chat__input-btn svg, - .agent-chat__toolbar .btn-ghost svg { - width: 14px; - height: 14px; - } - /* Log stream */ .log-stream { border-radius: var(--radius-md); - max-height: 320px; + max-height: 380px; } .log-row { grid-template-columns: 1fr; - gap: 2px; - padding: 6px; + gap: 4px; + padding: 8px; } .log-time { @@ -274,15 +282,7 @@ } .log-message { - font-size: 11px; - word-break: break-word; - } - - .ov-log-tail-content { - max-height: 200px; - font-size: 10px; - padding: 8px; - line-height: 1.5; + font-size: 12px; } /* Lists */ @@ -306,10 +306,18 @@ font-size: 11px; } - .theme-select { - height: 26px; - min-width: 78px; - font-size: 11px; + .theme-toggle { + height: 28px; + } + + .theme-btn svg { + width: 12px; + height: 12px; + } + + .theme-icon { + width: 12px; + height: 12px; } } @@ -372,9 +380,12 @@ padding: 3px 6px; } - .theme-select { - height: 24px; - min-width: 72px; - font-size: 10px; + .theme-toggle { + height: 26px; + } + + .theme-icon { + width: 11px; + height: 11px; } } diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index d3b1acf4496..7bea77067ed 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -13,72 +13,82 @@ function row(overrides: Partial & { key: string }): SessionRow { * ================================================================ */ describe("parseSessionKey", () => { - it("maps session keys to expected prefixes and fallback names", () => { - const cases = [ - { - name: "bare main", - key: "main", - expected: { prefix: "", fallbackName: "Main Session" }, - }, - { - name: "agent main key", - key: "agent:main:main", - expected: { prefix: "", fallbackName: "Main Session" }, - }, - { - name: "subagent key", - key: "agent:main:subagent:18abfefe-1fa6-43cb-8ba8-ebdc9b43e253", - expected: { prefix: "Subagent:", fallbackName: "Subagent:" }, - }, - { - name: "cron key", - key: "agent:main:cron:daily-briefing-uuid", - expected: { prefix: "Cron:", fallbackName: "Cron Job:" }, - }, - { - name: "direct known channel", - key: "agent:main:bluebubbles:direct:+19257864429", - expected: { prefix: "", fallbackName: "iMessage · +19257864429" }, - }, - { - name: "direct telegram", - key: "agent:main:telegram:direct:user123", - expected: { prefix: "", fallbackName: "Telegram · user123" }, - }, - { - name: "group known channel", - key: "agent:main:discord:group:guild-chan", - expected: { prefix: "", fallbackName: "Discord Group" }, - }, - { - name: "unknown channel direct", - key: "agent:main:mychannel:direct:user1", - expected: { prefix: "", fallbackName: "Mychannel · user1" }, - }, - { - name: "legacy channel-prefixed key", - key: "bluebubbles:g-agent-main-bluebubbles-direct-+19257864429", - expected: { prefix: "", fallbackName: "iMessage Session" }, - }, - { - name: "legacy discord key", - key: "discord:123:456", - expected: { prefix: "", fallbackName: "Discord Session" }, - }, - { - name: "bare channel key", - key: "telegram", - expected: { prefix: "", fallbackName: "Telegram Session" }, - }, - { - name: "unknown pattern", - key: "something-unknown", - expected: { prefix: "", fallbackName: "something-unknown" }, - }, - ] as const; - for (const testCase of cases) { - expect(parseSessionKey(testCase.key), testCase.name).toEqual(testCase.expected); - } + it("identifies main session (bare 'main')", () => { + expect(parseSessionKey("main")).toEqual({ prefix: "", fallbackName: "Main Session" }); + }); + + it("identifies main session (agent:main:main)", () => { + expect(parseSessionKey("agent:main:main")).toEqual({ + prefix: "", + fallbackName: "Main Session", + }); + }); + + it("identifies subagent sessions", () => { + expect(parseSessionKey("agent:main:subagent:18abfefe-1fa6-43cb-8ba8-ebdc9b43e253")).toEqual({ + prefix: "Subagent:", + fallbackName: "Subagent:", + }); + }); + + it("identifies cron sessions", () => { + expect(parseSessionKey("agent:main:cron:daily-briefing-uuid")).toEqual({ + prefix: "Cron:", + fallbackName: "Cron Job:", + }); + }); + + it("identifies direct chat with known channel", () => { + expect(parseSessionKey("agent:main:bluebubbles:direct:+19257864429")).toEqual({ + prefix: "", + fallbackName: "iMessage · +19257864429", + }); + }); + + it("identifies direct chat with telegram", () => { + expect(parseSessionKey("agent:main:telegram:direct:user123")).toEqual({ + prefix: "", + fallbackName: "Telegram · user123", + }); + }); + + it("identifies group chat with known channel", () => { + expect(parseSessionKey("agent:main:discord:group:guild-chan")).toEqual({ + prefix: "", + fallbackName: "Discord Group", + }); + }); + + it("capitalises unknown channels in direct/group patterns", () => { + expect(parseSessionKey("agent:main:mychannel:direct:user1")).toEqual({ + prefix: "", + fallbackName: "Mychannel · user1", + }); + }); + + it("identifies channel-prefixed legacy keys", () => { + expect(parseSessionKey("bluebubbles:g-agent-main-bluebubbles-direct-+19257864429")).toEqual({ + prefix: "", + fallbackName: "iMessage Session", + }); + expect(parseSessionKey("discord:123:456")).toEqual({ + prefix: "", + fallbackName: "Discord Session", + }); + }); + + it("handles bare channel name as key", () => { + expect(parseSessionKey("telegram")).toEqual({ + prefix: "", + fallbackName: "Telegram Session", + }); + }); + + it("returns raw key for unknown patterns", () => { + expect(parseSessionKey("something-unknown")).toEqual({ + prefix: "", + fallbackName: "something-unknown", + }); }); }); @@ -87,130 +97,167 @@ describe("parseSessionKey", () => { * ================================================================ */ describe("resolveSessionDisplayName", () => { - it("resolves key-only fallbacks", () => { - const cases = [ - { key: "agent:main:main", expected: "Main Session" }, - { key: "main", expected: "Main Session" }, - { key: "agent:main:subagent:abc-123", expected: "Subagent:" }, - { key: "agent:main:cron:abc-123", expected: "Cron Job:" }, - { key: "agent:main:bluebubbles:direct:+19257864429", expected: "iMessage · +19257864429" }, - { key: "discord:123:456", expected: "Discord Session" }, - { key: "something-custom", expected: "something-custom" }, - ] as const; - for (const testCase of cases) { - expect(resolveSessionDisplayName(testCase.key), testCase.key).toBe(testCase.expected); - } + // ── Key-only fallbacks (no row) ────────────────── + + it("returns 'Main Session' for agent:main:main key", () => { + expect(resolveSessionDisplayName("agent:main:main")).toBe("Main Session"); }); - it("resolves row labels/display names and typed prefixes", () => { - const cases = [ - { - name: "row with no label/display", - key: "agent:main:main", - rowData: row({ key: "agent:main:main" }), - expected: "Main Session", - }, - { - name: "displayName equals key", - key: "mykey", - rowData: row({ key: "mykey", displayName: "mykey" }), - expected: "mykey", - }, - { - name: "label equals key", - key: "mykey", - rowData: row({ key: "mykey", label: "mykey" }), - expected: "mykey", - }, - { - name: "label used", - key: "discord:123:456", - rowData: row({ key: "discord:123:456", label: "General" }), - expected: "General", - }, - { - name: "displayName fallback", - key: "discord:123:456", - rowData: row({ key: "discord:123:456", displayName: "My Chat" }), - expected: "My Chat", - }, - { - name: "label preferred over displayName", - key: "discord:123:456", - rowData: row({ key: "discord:123:456", displayName: "My Chat", label: "General" }), - expected: "General", - }, - { - name: "ignore whitespace label", - key: "discord:123:456", - rowData: row({ key: "discord:123:456", displayName: "My Chat", label: " " }), - expected: "My Chat", - }, - { - name: "fallback when whitespace label and no displayName", - key: "discord:123:456", - rowData: row({ key: "discord:123:456", label: " " }), - expected: "Discord Session", - }, - { - name: "trim label", - key: "k", - rowData: row({ key: "k", label: " General " }), - expected: "General", - }, - { - name: "trim displayName", - key: "k", - rowData: row({ key: "k", displayName: " My Chat " }), - expected: "My Chat", - }, - { - name: "prefix subagent label", - key: "agent:main:subagent:abc-123", - rowData: row({ key: "agent:main:subagent:abc-123", label: "maintainer-v2" }), - expected: "Subagent: maintainer-v2", - }, - { - name: "prefix subagent displayName", - key: "agent:main:subagent:abc-123", - rowData: row({ key: "agent:main:subagent:abc-123", displayName: "Task Runner" }), - expected: "Subagent: Task Runner", - }, - { - name: "prefix cron label", - key: "agent:main:cron:abc-123", - rowData: row({ key: "agent:main:cron:abc-123", label: "daily-briefing" }), - expected: "Cron: daily-briefing", - }, - { - name: "prefix cron displayName", - key: "agent:main:cron:abc-123", - rowData: row({ key: "agent:main:cron:abc-123", displayName: "Nightly Sync" }), - expected: "Cron: Nightly Sync", - }, - { - name: "avoid double cron prefix", - key: "agent:main:cron:abc-123", - rowData: row({ key: "agent:main:cron:abc-123", label: "Cron: Nightly Sync" }), - expected: "Cron: Nightly Sync", - }, - { - name: "avoid double subagent prefix", - key: "agent:main:subagent:abc-123", - rowData: row({ key: "agent:main:subagent:abc-123", displayName: "Subagent: Runner" }), - expected: "Subagent: Runner", - }, - { - name: "non-typed label without prefix", - key: "agent:main:bluebubbles:direct:+19257864429", - rowData: row({ key: "agent:main:bluebubbles:direct:+19257864429", label: "Tyler" }), - expected: "Tyler", - }, - ] as const; - for (const testCase of cases) { - expect(resolveSessionDisplayName(testCase.key, testCase.rowData), testCase.name).toBe( - testCase.expected, - ); - } + it("returns 'Main Session' for bare 'main' key", () => { + expect(resolveSessionDisplayName("main")).toBe("Main Session"); + }); + + it("returns 'Subagent:' for subagent key without row", () => { + expect(resolveSessionDisplayName("agent:main:subagent:abc-123")).toBe("Subagent:"); + }); + + it("returns 'Cron Job:' for cron key without row", () => { + expect(resolveSessionDisplayName("agent:main:cron:abc-123")).toBe("Cron Job:"); + }); + + it("parses direct chat key with channel", () => { + expect(resolveSessionDisplayName("agent:main:bluebubbles:direct:+19257864429")).toBe( + "iMessage · +19257864429", + ); + }); + + it("parses channel-prefixed legacy key", () => { + expect(resolveSessionDisplayName("discord:123:456")).toBe("Discord Session"); + }); + + it("returns raw key for unknown patterns", () => { + expect(resolveSessionDisplayName("something-custom")).toBe("something-custom"); + }); + + // ── With row data (label / displayName) ────────── + + it("returns parsed fallback when row has no label or displayName", () => { + expect(resolveSessionDisplayName("agent:main:main", row({ key: "agent:main:main" }))).toBe( + "Main Session", + ); + }); + + it("returns parsed fallback when displayName matches key", () => { + expect(resolveSessionDisplayName("mykey", row({ key: "mykey", displayName: "mykey" }))).toBe( + "mykey", + ); + }); + + it("returns parsed fallback when label matches key", () => { + expect(resolveSessionDisplayName("mykey", row({ key: "mykey", label: "mykey" }))).toBe("mykey"); + }); + + it("uses label alone when available", () => { + expect( + resolveSessionDisplayName( + "discord:123:456", + row({ key: "discord:123:456", label: "General" }), + ), + ).toBe("General"); + }); + + it("falls back to displayName when label is absent", () => { + expect( + resolveSessionDisplayName( + "discord:123:456", + row({ key: "discord:123:456", displayName: "My Chat" }), + ), + ).toBe("My Chat"); + }); + + it("prefers label over displayName when both are present", () => { + expect( + resolveSessionDisplayName( + "discord:123:456", + row({ key: "discord:123:456", displayName: "My Chat", label: "General" }), + ), + ).toBe("General"); + }); + + it("ignores whitespace-only label and falls back to displayName", () => { + expect( + resolveSessionDisplayName( + "discord:123:456", + row({ key: "discord:123:456", displayName: "My Chat", label: " " }), + ), + ).toBe("My Chat"); + }); + + it("uses parsed fallback when whitespace-only label and no displayName", () => { + expect( + resolveSessionDisplayName("discord:123:456", row({ key: "discord:123:456", label: " " })), + ).toBe("Discord Session"); + }); + + it("trims label and displayName", () => { + expect(resolveSessionDisplayName("k", row({ key: "k", label: " General " }))).toBe("General"); + expect(resolveSessionDisplayName("k", row({ key: "k", displayName: " My Chat " }))).toBe( + "My Chat", + ); + }); + + // ── Type prefixes applied to labels / displayNames ── + + it("prefixes subagent label with Subagent:", () => { + expect( + resolveSessionDisplayName( + "agent:main:subagent:abc-123", + row({ key: "agent:main:subagent:abc-123", label: "maintainer-v2" }), + ), + ).toBe("Subagent: maintainer-v2"); + }); + + it("prefixes subagent displayName with Subagent:", () => { + expect( + resolveSessionDisplayName( + "agent:main:subagent:abc-123", + row({ key: "agent:main:subagent:abc-123", displayName: "Task Runner" }), + ), + ).toBe("Subagent: Task Runner"); + }); + + it("prefixes cron label with Cron:", () => { + expect( + resolveSessionDisplayName( + "agent:main:cron:abc-123", + row({ key: "agent:main:cron:abc-123", label: "daily-briefing" }), + ), + ).toBe("Cron: daily-briefing"); + }); + + it("prefixes cron displayName with Cron:", () => { + expect( + resolveSessionDisplayName( + "agent:main:cron:abc-123", + row({ key: "agent:main:cron:abc-123", displayName: "Nightly Sync" }), + ), + ).toBe("Cron: Nightly Sync"); + }); + + it("does not double-prefix cron labels that already include Cron:", () => { + expect( + resolveSessionDisplayName( + "agent:main:cron:abc-123", + row({ key: "agent:main:cron:abc-123", label: "Cron: Nightly Sync" }), + ), + ).toBe("Cron: Nightly Sync"); + }); + + it("does not double-prefix subagent display names that already include Subagent:", () => { + expect( + resolveSessionDisplayName( + "agent:main:subagent:abc-123", + row({ key: "agent:main:subagent:abc-123", displayName: "Subagent: Runner" }), + ), + ).toBe("Subagent: Runner"); + }); + + it("does not prefix non-typed sessions with labels", () => { + expect( + resolveSessionDisplayName( + "agent:main:bluebubbles:direct:+19257864429", + row({ key: "agent:main:bluebubbles:direct:+19257864429", label: "Tyler" }), + ), + ).toBe("Tyler"); }); }); diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 890611483fd..d7610962872 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -84,59 +84,18 @@ export function renderTab(state: AppViewState, tab: Tab) { `; } -export function renderChatSessionSelect(state: AppViewState) { +export function renderChatControls(state: AppViewState) { const mainSessionKey = resolveMainSessionKey(state.hello, state.sessionsResult); const sessionOptions = resolveSessionOptions( state.sessionKey, state.sessionsResult, mainSessionKey, ); - return html` - - `; -} - -export function renderChatControls(state: AppViewState) { const disableThinkingToggle = state.onboarding; const disableFocusToggle = state.onboarding; const showThinking = state.onboarding ? false : state.settings.chatShowThinking; const focusActive = state.onboarding ? true : state.settings.chatFocusMode; + // Refresh icon const refreshIcon = html` +
{ + const toggle = e.currentTarget as HTMLElement; + requestAnimationFrame(() => { + if (!toggle.contains(document.activeElement)) { + handleCollapse(); + } + }); }} > - ${THEME_OPTIONS.map((opt) => html``)} - + ${state.themeOrder.map((id) => { + const opt = THEME_OPTIONS.find((o) => o.id === id)!; + return html` + + `; + })} +
`; } diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 78a75719be0..b56dea7a89b 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -6,12 +6,7 @@ import { import { t } from "../i18n/index.ts"; import { refreshChatAvatar } from "./app-chat.ts"; import { renderUsageTab } from "./app-render-usage-tab.ts"; -import { - renderChatControls, - renderChatSessionSelect, - renderTab, - renderThemeToggle, -} from "./app-render.helpers.ts"; +import { renderChatControls, renderTab, renderThemeToggle } from "./app-render.helpers.ts"; import type { AppViewState } from "./app-view-state.ts"; import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts"; import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts"; @@ -64,6 +59,7 @@ 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"; @@ -83,33 +79,6 @@ import { renderSkills } from "./views/skills.ts"; const AVATAR_DATA_RE = /^data:/i; const AVATAR_HTTP_RE = /^https?:\/\//i; -const NAV_WIDTH_MIN = 180; -const NAV_WIDTH_MAX = 400; - -function handleNavResizeStart(e: MouseEvent, state: AppViewState) { - e.preventDefault(); - const startX = e.clientX; - const startWidth = state.settings.navWidth; - - const onMove = (ev: MouseEvent) => { - const delta = ev.clientX - startX; - const next = Math.round(Math.min(NAV_WIDTH_MAX, Math.max(NAV_WIDTH_MIN, startWidth + delta))); - state.applySettings({ ...state.settings, navWidth: next }); - }; - - const onUp = () => { - document.removeEventListener("mousemove", onMove); - document.removeEventListener("mouseup", onUp); - document.body.style.cursor = ""; - document.body.style.userSelect = ""; - }; - - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - document.addEventListener("mousemove", onMove); - document.addEventListener("mouseup", onUp); -} - function resolveAssistantAvatarUrl(state: AppViewState): string | undefined { const list = state.agentsList?.agents ?? []; const parsed = parseAgentSessionKey(state.sessionKey); @@ -171,15 +140,11 @@ export function renderApp(state: AppViewState) { onNavigate: (tab) => { state.setTab(tab as import("./navigation.ts").Tab); }, - onSlashCommand: (cmd) => { + onSlashCommand: (_cmd) => { state.setTab("chat" as import("./navigation.ts").Tab); - state.chatMessage = cmd.endsWith(" ") ? cmd : `${cmd} `; }, })} -
+
+ +
${state.connected ? t("common.ok") : t("common.offline")} @@ -202,7 +184,6 @@ export function renderApp(state: AppViewState) { ${renderThemeToggle(state)}
-
- ${ - !state.settings.navCollapsed && !chatFocus - ? html` - - ` - : nothing - } -
${ state.updateAvailable @@ -339,14 +301,8 @@ export function renderApp(state: AppViewState) { }
- ${ - isChat - ? renderChatSessionSelect(state) - : state.tab === "skills" - ? nothing - : html`
${titleForTab(state.tab)}
` - } - ${isChat || state.tab === "skills" ? nothing : html`
${subtitleForTab(state.tab)}
`} + ${state.tab === "usage" ? nothing : html`
${titleForTab(state.tab)}
`} + ${state.tab === "usage" ? nothing : html`
${subtitleForTab(state.tab)}
`}
${state.lastError ? html`
${state.lastError}
` : nothing} @@ -468,37 +424,12 @@ export function renderApp(state: AppViewState) { includeGlobal: state.sessionsIncludeGlobal, includeUnknown: state.sessionsIncludeUnknown, basePath: state.basePath, - searchQuery: state.sessionsSearchQuery, - sortColumn: state.sessionsSortColumn, - sortDir: state.sessionsSortDir, - page: state.sessionsPage, - pageSize: state.sessionsPageSize, - actionsOpenKey: state.sessionsActionsOpenKey, onFiltersChange: (next) => { state.sessionsFilterActive = next.activeMinutes; state.sessionsFilterLimit = next.limit; state.sessionsIncludeGlobal = next.includeGlobal; state.sessionsIncludeUnknown = next.includeUnknown; }, - onSearchChange: (q) => { - state.sessionsSearchQuery = q; - state.sessionsPage = 0; - }, - onSortChange: (col, dir) => { - state.sessionsSortColumn = col; - state.sessionsSortDir = dir; - state.sessionsPage = 0; - }, - onPageChange: (p) => { - state.sessionsPage = p; - }, - onPageSizeChange: (s) => { - state.sessionsPageSize = s; - state.sessionsPage = 0; - }, - onActionsOpenChange: (key) => { - state.sessionsActionsOpenKey = key; - }, onRefresh: () => loadSessions(state), onPatch: (key, patch) => patchSession(state, key, patch), onDelete: (key) => deleteSessionAndRefresh(state, key), @@ -540,7 +471,6 @@ export function renderApp(state: AppViewState) { ${ state.tab === "agents" ? renderAgents({ - basePath: state.basePath ?? "", loading: state.agentsLoading, error: state.agentsError, agentsList: state.agentsList, @@ -583,6 +513,10 @@ export function renderApp(state: AppViewState) { agentId: state.agentSkillsAgentId, filter: state.skillsFilter, }, + sidebarFilter: state.agentsSidebarFilter, + onSidebarFilterChange: (value) => { + state.agentsSidebarFilter = value; + }, onRefresh: async () => { await loadAgents(state); const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? []; @@ -1118,7 +1052,6 @@ export function renderApp(state: AppViewState) { onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio), assistantName: state.assistantName, assistantAvatar: state.assistantAvatar, - basePath: state.basePath ?? "", }) : nothing } @@ -1210,7 +1143,10 @@ export function renderApp(state: AppViewState) {
${renderExecApprovalPrompt(state)} ${renderGatewayUrlConfirmation(state)} - ${nothing} + ${renderBottomTabs({ + activeTab: state.tab, + onTabChange: (tab) => state.setTab(tab), + })}
`; } diff --git a/ui/src/ui/app-scroll.test.ts b/ui/src/ui/app-scroll.test.ts index 244d61c3587..111b54de93a 100644 --- a/ui/src/ui/app-scroll.test.ts +++ b/ui/src/ui/app-scroll.test.ts @@ -61,39 +61,45 @@ function createScrollEvent(scrollHeight: number, scrollTop: number, clientHeight /* ------------------------------------------------------------------ */ describe("handleChatScroll", () => { - it("updates near-bottom state across threshold boundaries", () => { - const cases = [ - { - name: "clearly near bottom", - event: createScrollEvent(2000, 1600, 400), - expected: true, - }, - { - name: "just under threshold", - event: createScrollEvent(2000, 1151, 400), - expected: true, - }, - { - name: "exactly at threshold", - event: createScrollEvent(2000, 1150, 400), - expected: false, - }, - { - name: "well above threshold", - event: createScrollEvent(2000, 500, 400), - expected: false, - }, - { - name: "scrolled up beyond long message", - event: createScrollEvent(2000, 1100, 400), - expected: false, - }, - ] as const; - for (const testCase of cases) { - const { host } = createScrollHost({}); - handleChatScroll(host, testCase.event); - expect(host.chatUserNearBottom, testCase.name).toBe(testCase.expected); - } + it("sets chatUserNearBottom=true when within the 450px threshold", () => { + const { host } = createScrollHost({}); + // distanceFromBottom = 2000 - 1600 - 400 = 0 → clearly near bottom + const event = createScrollEvent(2000, 1600, 400); + handleChatScroll(host, event); + expect(host.chatUserNearBottom).toBe(true); + }); + + it("sets chatUserNearBottom=true when distance is just under threshold", () => { + const { host } = createScrollHost({}); + // distanceFromBottom = 2000 - 1151 - 400 = 449 → just under threshold + const event = createScrollEvent(2000, 1151, 400); + handleChatScroll(host, event); + expect(host.chatUserNearBottom).toBe(true); + }); + + it("sets chatUserNearBottom=false when distance is exactly at threshold", () => { + const { host } = createScrollHost({}); + // distanceFromBottom = 2000 - 1150 - 400 = 450 → at threshold (uses strict <) + const event = createScrollEvent(2000, 1150, 400); + handleChatScroll(host, event); + expect(host.chatUserNearBottom).toBe(false); + }); + + it("sets chatUserNearBottom=false when scrolled well above threshold", () => { + const { host } = createScrollHost({}); + // distanceFromBottom = 2000 - 500 - 400 = 1100 → way above threshold + const event = createScrollEvent(2000, 500, 400); + handleChatScroll(host, event); + expect(host.chatUserNearBottom).toBe(false); + }); + + it("sets chatUserNearBottom=false when user scrolled up past one long message (>200px <450px)", () => { + const { host } = createScrollHost({}); + // distanceFromBottom = 2000 - 1250 - 400 = 350 → old threshold would say "near", new says "near" + // distanceFromBottom = 2000 - 1100 - 400 = 500 → old threshold would say "not near", new also "not near" + const event = createScrollEvent(2000, 1100, 400); + handleChatScroll(host, event); + expect(host.chatUserNearBottom).toBe(false); }); }); @@ -115,67 +121,85 @@ describe("scheduleChatScroll", () => { vi.restoreAllMocks(); }); - it("respects near-bottom, force, and initial-load behavior", async () => { - const cases = [ - { - name: "near-bottom auto-scroll", - scrollTop: 1600, - chatUserNearBottom: true, - chatHasAutoScrolled: false, - force: false, - expectedScrollsToBottom: true, - expectedNewMessagesBelow: false, - }, - { - name: "scrolled-up no-force", - scrollTop: 500, - chatUserNearBottom: false, - chatHasAutoScrolled: false, - force: false, - expectedScrollsToBottom: false, - expectedNewMessagesBelow: true, - }, - { - name: "scrolled-up force after initial load", - scrollTop: 500, - chatUserNearBottom: false, - chatHasAutoScrolled: true, - force: true, - expectedScrollsToBottom: false, - expectedNewMessagesBelow: true, - }, - { - name: "scrolled-up force on initial load", - scrollTop: 500, - chatUserNearBottom: false, - chatHasAutoScrolled: false, - force: true, - expectedScrollsToBottom: true, - expectedNewMessagesBelow: false, - }, - ] as const; + it("scrolls to bottom when user is near bottom (no force)", async () => { + const { host, container } = createScrollHost({ + scrollHeight: 2000, + scrollTop: 1600, + clientHeight: 400, + }); + // distanceFromBottom = 2000 - 1600 - 400 = 0 → near bottom + host.chatUserNearBottom = true; - for (const testCase of cases) { - const { host, container } = createScrollHost({ - scrollHeight: 2000, - scrollTop: testCase.scrollTop, - clientHeight: 400, - }); - host.chatUserNearBottom = testCase.chatUserNearBottom; - host.chatHasAutoScrolled = testCase.chatHasAutoScrolled; - host.chatNewMessagesBelow = false; - const originalScrollTop = container.scrollTop; + scheduleChatScroll(host); + await host.updateComplete; - scheduleChatScroll(host, testCase.force); - await host.updateComplete; + expect(container.scrollTop).toBe(container.scrollHeight); + }); - if (testCase.expectedScrollsToBottom) { - expect(container.scrollTop, testCase.name).toBe(container.scrollHeight); - } else { - expect(container.scrollTop, testCase.name).toBe(originalScrollTop); - } - expect(host.chatNewMessagesBelow, testCase.name).toBe(testCase.expectedNewMessagesBelow); - } + it("does NOT scroll when user is scrolled up and no force", async () => { + const { host, container } = createScrollHost({ + scrollHeight: 2000, + scrollTop: 500, + clientHeight: 400, + }); + // distanceFromBottom = 2000 - 500 - 400 = 1100 → not near bottom + host.chatUserNearBottom = false; + const originalScrollTop = container.scrollTop; + + scheduleChatScroll(host); + await host.updateComplete; + + expect(container.scrollTop).toBe(originalScrollTop); + }); + + it("does NOT scroll with force=true when user has explicitly scrolled up", async () => { + const { host, container } = createScrollHost({ + scrollHeight: 2000, + scrollTop: 500, + clientHeight: 400, + }); + // User has scrolled up — chatUserNearBottom is false + host.chatUserNearBottom = false; + host.chatHasAutoScrolled = true; // Already past initial load + const originalScrollTop = container.scrollTop; + + scheduleChatScroll(host, true); + await host.updateComplete; + + // force=true should still NOT override explicit user scroll-up after initial load + expect(container.scrollTop).toBe(originalScrollTop); + }); + + it("DOES scroll with force=true on initial load (chatHasAutoScrolled=false)", async () => { + const { host, container } = createScrollHost({ + scrollHeight: 2000, + scrollTop: 500, + clientHeight: 400, + }); + host.chatUserNearBottom = false; + host.chatHasAutoScrolled = false; // Initial load + + scheduleChatScroll(host, true); + await host.updateComplete; + + // On initial load, force should work regardless + expect(container.scrollTop).toBe(container.scrollHeight); + }); + + it("sets chatNewMessagesBelow when not scrolling due to user position", async () => { + const { host } = createScrollHost({ + scrollHeight: 2000, + scrollTop: 500, + clientHeight: 400, + }); + host.chatUserNearBottom = false; + host.chatHasAutoScrolled = true; + host.chatNewMessagesBelow = false; + + scheduleChatScroll(host); + await host.updateComplete; + + expect(host.chatNewMessagesBelow).toBe(true); }); }); diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts index 051eac27df0..e1b05791306 100644 --- a/ui/src/ui/app-settings.test.ts +++ b/ui/src/ui/app-settings.test.ts @@ -19,7 +19,6 @@ const createHost = (tab: Tab): SettingsHost => ({ splitRatio: 0.6, navCollapsed: false, navGroupsCollapsed: {}, - navWidth: 220, }, theme: "dark", themeResolved: "dark", diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 1e7a8a6ad31..1d50cd9852c 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -269,7 +269,7 @@ export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) } const root = document.documentElement; root.dataset.theme = resolved; - root.style.colorScheme = resolved === "light" ? "light" : "dark"; + root.style.colorScheme = "dark"; } export function syncTabWithLocation(host: SettingsHost, replace: boolean) { diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 34c404a4e77..5ee23477ba6 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -146,6 +146,7 @@ export type AppViewState = { agentSkillsError: string | null; agentSkillsReport: SkillStatusReport | null; agentSkillsAgentId: string | null; + agentsSidebarFilter: string; sessionsLoading: boolean; sessionsResult: SessionsListResult | null; sessionsError: string | null; @@ -153,12 +154,6 @@ export type AppViewState = { sessionsFilterLimit: string; sessionsIncludeGlobal: boolean; sessionsIncludeUnknown: boolean; - sessionsSearchQuery: string; - sessionsSortColumn: "key" | "kind" | "updated" | "tokens"; - sessionsSortDir: "asc" | "desc"; - sessionsPage: number; - sessionsPageSize: number; - sessionsActionsOpenKey: string | null; usageLoading: boolean; usageResult: SessionsUsageResult | null; usageCostSummary: CostUsageSummary | null; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 275ff979479..1c284079c93 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -231,6 +231,7 @@ 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; @@ -239,12 +240,6 @@ export class OpenClawApp extends LitElement { @state() sessionsFilterLimit = "120"; @state() sessionsIncludeGlobal = true; @state() sessionsIncludeUnknown = false; - @state() sessionsSearchQuery = ""; - @state() sessionsSortColumn: "key" | "kind" | "updated" | "tokens" = "updated"; - @state() sessionsSortDir: "asc" | "desc" = "desc"; - @state() sessionsPage = 0; - @state() sessionsPageSize = 10; - @state() sessionsActionsOpenKey: string | null = null; @state() usageLoading = false; @state() usageResult: import("./types.js").SessionsUsageResult | null = null; @@ -469,6 +464,12 @@ export class OpenClawApp extends LitElement { return [active, ...rest]; } + handleThemeToggleCollapse() { + setTimeout(() => { + this.themeOrder = this.buildThemeOrder(this.theme); + }, 80); + } + async loadOverview() { await loadOverviewInternal(this as unknown as Parameters[0]); } diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 160e36a5ef5..0eb3f2251f8 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -5,7 +5,6 @@ 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 { agentLogoUrl } from "../views/agents-utils.ts"; import { renderCopyAsMarkdownButton } from "./copy-as-markdown.ts"; import { extractTextCached, @@ -57,10 +56,10 @@ function extractImages(message: unknown): ImageBlock[] { return images; } -export function renderReadingIndicatorGroup(assistant?: AssistantIdentity, basePath?: string) { +export function renderReadingIndicatorGroup(assistant?: AssistantIdentity) { return html`
- ${renderAvatar("assistant", assistant, basePath)} + ${renderAvatar("assistant", assistant)}