From 1e8681497c771c8d55d58dea4580cd3eed69ad3d Mon Sep 17 00:00:00 2001 From: Cyrus Goh Date: Sun, 25 Jan 2026 13:38:05 -0800 Subject: [PATCH] chore(ui): remove unused imports and variables Clean up all unused imports, variables, and parameters across the UI codebase to comply with the new noUnusedLocals and noUnusedParameters TypeScript compiler options. --- ui/src/styles/base.css | 348 ++----- ui/src/styles/chat/grouped.css | 11 +- ui/src/styles/chat/layout.css | 5 +- ui/src/styles/chat/tool-cards.css | 72 +- ui/src/styles/components.css | 917 ++++++++---------- ui/src/styles/config.css | 640 ++++++------ ui/src/styles/layout.css | 319 +++--- ui/src/ui/app-channels.ts | 19 +- ui/src/ui/app-chat.ts | 9 +- ui/src/ui/app-gateway.ts | 14 +- ui/src/ui/app-lifecycle.ts | 26 +- ui/src/ui/app-render.helpers.ts | 51 +- ui/src/ui/app-render.ts | 644 ++++++------ ui/src/ui/app-scroll.ts | 12 +- ui/src/ui/app-settings.ts | 39 +- ui/src/ui/app-tool-stream.ts | 3 +- ui/src/ui/app-view-state.ts | 5 +- ui/src/ui/app.ts | 51 +- ui/src/ui/assistant-identity.ts | 7 +- ui/src/ui/chat-markdown.browser.test.ts | 4 +- ui/src/ui/chat/copy-as-markdown.ts | 18 +- ui/src/ui/chat/grouped-render.ts | 32 +- ui/src/ui/chat/message-extract.ts | 8 +- ui/src/ui/chat/message-normalizer.test.ts | 14 +- ui/src/ui/chat/message-normalizer.ts | 8 +- ui/src/ui/chat/tool-cards.ts | 76 +- ui/src/ui/chat/tool-helpers.test.ts | 2 +- ui/src/ui/components/resizable-divider.ts | 12 +- ui/src/ui/config-form.browser.test.ts | 24 +- ui/src/ui/controllers/assistant-identity.ts | 5 +- ui/src/ui/controllers/chat.ts | 9 +- ui/src/ui/controllers/config.test.ts | 10 +- ui/src/ui/controllers/config.ts | 29 +- ui/src/ui/controllers/config/form-utils.ts | 10 +- ui/src/ui/controllers/cron.ts | 9 +- ui/src/ui/controllers/debug.ts | 4 +- ui/src/ui/controllers/devices.ts | 4 +- ui/src/ui/controllers/exec-approval.ts | 5 +- ui/src/ui/controllers/exec-approvals.ts | 9 +- ui/src/ui/controllers/logs.ts | 22 +- ui/src/ui/controllers/nodes.ts | 5 +- ui/src/ui/controllers/presence.ts | 4 +- ui/src/ui/controllers/skills.ts | 16 +- ui/src/ui/focus-mode.browser.test.ts | 8 +- ui/src/ui/format.ts | 5 +- ui/src/ui/icons.ts | 248 ++++- ui/src/ui/navigation.browser.test.ts | 8 +- ui/src/ui/navigation.ts | 4 +- ui/src/ui/storage.ts | 21 +- ui/src/ui/theme-transition.ts | 9 +- ui/src/ui/theme.ts | 4 +- ui/src/ui/tool-display.json | 18 +- ui/src/ui/tool-display.ts | 7 +- ui/src/ui/types.ts | 8 +- ui/src/ui/views/channels.config.ts | 48 +- ui/src/ui/views/channels.discord.ts | 16 +- ui/src/ui/views/channels.googlechat.ts | 24 +- ui/src/ui/views/channels.imessage.ts | 16 +- .../ui/views/channels.nostr-profile-form.ts | 46 +- ui/src/ui/views/channels.nostr.ts | 101 +- ui/src/ui/views/channels.signal.ts | 16 +- ui/src/ui/views/channels.slack.ts | 16 +- ui/src/ui/views/channels.telegram.ts | 34 +- ui/src/ui/views/channels.ts | 75 +- ui/src/ui/views/channels.types.ts | 24 +- ui/src/ui/views/channels.whatsapp.ts | 32 +- ui/src/ui/views/chat.ts | 110 ++- ui/src/ui/views/config-form.analyze.ts | 20 +- ui/src/ui/views/config-form.node.ts | 246 +++-- ui/src/ui/views/config-form.render.ts | 340 +++++-- ui/src/ui/views/config-form.ts | 5 +- ui/src/ui/views/config.browser.test.ts | 62 +- ui/src/ui/views/config.ts | 439 ++++++--- ui/src/ui/views/cron.ts | 72 +- ui/src/ui/views/debug.ts | 31 +- ui/src/ui/views/exec-approval.ts | 16 +- ui/src/ui/views/instances.ts | 43 +- ui/src/ui/views/logs.ts | 51 +- ui/src/ui/views/markdown-sidebar.ts | 16 +- ui/src/ui/views/nodes.ts | 278 +++--- ui/src/ui/views/overview.ts | 51 +- ui/src/ui/views/sessions.ts | 36 +- ui/src/ui/views/skills.ts | 82 +- 83 files changed, 3204 insertions(+), 3013 deletions(-) diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index 7baab70a2..c3ec4cbc7 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -1,189 +1,61 @@ -@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap"); :root { - /* Background - Warmer dark with depth */ - --bg: #12141a; - --bg-accent: #14161d; - --bg-elevated: #1a1d25; - --bg-hover: #262a35; - --bg-muted: #262a35; - - /* Card / Surface - More contrast between levels */ - --card: #181b22; - --card-foreground: #f4f4f5; - --card-highlight: rgba(255, 255, 255, 0.05); - --popover: #181b22; - --popover-foreground: #f4f4f5; - - /* Panel */ - --panel: #12141a; - --panel-strong: #1a1d25; - --panel-hover: #262a35; - --chrome: rgba(18, 20, 26, 0.95); - --chrome-strong: rgba(18, 20, 26, 0.98); - - /* Text - Slightly warmer */ - --text: #e4e4e7; - --text-strong: #fafafa; - --chat-text: #e4e4e7; - --muted: #71717a; - --muted-strong: #52525b; - --muted-foreground: #71717a; - - /* Border - Subtle but defined */ - --border: #27272a; - --border-strong: #3f3f46; - --border-hover: #52525b; - --input: #27272a; - --ring: #ff5c5c; - - /* Accent - Punchy signature red */ - --accent: #ff5c5c; - --accent-hover: #ff7070; - --accent-muted: #ff5c5c; - --accent-subtle: rgba(255, 92, 92, 0.15); - --accent-foreground: #fafafa; - --accent-glow: rgba(255, 92, 92, 0.25); - --primary: #ff5c5c; - --primary-foreground: #ffffff; - - /* Secondary - Teal accent for variety */ - --secondary: #1e2028; - --secondary-foreground: #f4f4f5; - --accent-2: #14b8a6; - --accent-2-muted: rgba(20, 184, 166, 0.7); - --accent-2-subtle: rgba(20, 184, 166, 0.15); - - /* Semantic - More saturated */ - --ok: #22c55e; - --ok-muted: rgba(34, 197, 94, 0.75); - --ok-subtle: rgba(34, 197, 94, 0.12); - --destructive: #ef4444; - --destructive-foreground: #fafafa; - --warn: #f59e0b; - --warn-muted: rgba(245, 158, 11, 0.75); - --warn-subtle: rgba(245, 158, 11, 0.12); - --danger: #ef4444; - --danger-muted: rgba(239, 68, 68, 0.75); - --danger-subtle: rgba(239, 68, 68, 0.12); - --info: #3b82f6; - - /* Focus - With glow */ - --focus: rgba(255, 92, 92, 0.25); - --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring); - --focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 20px var(--accent-glow); - - /* Grid */ + --bg: #0a0f14; + --bg-accent: #111826; + --bg-grad-1: #162031; + --bg-grad-2: #1f2a22; + --bg-overlay: rgba(255, 255, 255, 0.05); + --bg-glow: rgba(245, 159, 74, 0.12); + --panel: rgba(14, 20, 30, 0.88); + --panel-strong: rgba(18, 26, 38, 0.96); + --chrome: rgba(9, 14, 20, 0.72); + --chrome-strong: rgba(9, 14, 20, 0.86); + --text: rgba(244, 246, 251, 0.96); + --chat-text: rgba(231, 237, 244, 0.92); + --muted: rgba(156, 169, 189, 0.72); + --border: rgba(255, 255, 255, 0.09); + --border-strong: rgba(255, 255, 255, 0.16); + --accent: #f59f4a; + --accent-2: #34c7b7; + --ok: #2bd97f; + --warn: #f2c94c; + --danger: #ff6b6b; + --focus: rgba(245, 159, 74, 0.35); --grid-line: rgba(255, 255, 255, 0.04); - - /* Theme transition */ --theme-switch-x: 50%; --theme-switch-y: 50%; - - /* Typography - Space Grotesk for personality */ - --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; - --font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - --font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - - /* Shadows - Richer with subtle color */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.03); - --shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.03); - --shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.03); - --shadow-glow: 0 0 30px var(--accent-glow); - - /* Radii - Slightly larger for friendlier feel */ - --radius-sm: 6px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; - --radius-full: 9999px; - --radius: 8px; - - /* Transitions - Snappy but smooth */ - --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); - --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); - --duration-fast: 120ms; - --duration-normal: 200ms; - --duration-slow: 350ms; - + --mono: + "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --font-body: "Work Sans", system-ui, sans-serif; + --font-display: "Unbounded", "Times New Roman", serif; color-scheme: dark; } -/* Light theme - Clean with subtle warmth */ :root[data-theme="light"] { - --bg: #fafafa; - --bg-accent: #f5f5f5; - --bg-elevated: #ffffff; - --bg-hover: #f0f0f0; - --bg-muted: #f0f0f0; - --bg-content: #f5f5f5; - - --card: #ffffff; - --card-foreground: #18181b; - --card-highlight: rgba(0, 0, 0, 0.03); - --popover: #ffffff; - --popover-foreground: #18181b; - - --panel: #fafafa; - --panel-strong: #f5f5f5; - --panel-hover: #ebebeb; - --chrome: rgba(250, 250, 250, 0.95); - --chrome-strong: rgba(250, 250, 250, 0.98); - - --text: #3f3f46; - --text-strong: #18181b; - --chat-text: #3f3f46; - --muted: #71717a; - --muted-strong: #52525b; - --muted-foreground: #71717a; - - --border: #e4e4e7; - --border-strong: #d4d4d8; - --border-hover: #a1a1aa; - --input: #e4e4e7; - - --accent: #dc2626; - --accent-hover: #ef4444; - --accent-muted: #dc2626; - --accent-subtle: rgba(220, 38, 38, 0.12); - --accent-foreground: #ffffff; - --accent-glow: rgba(220, 38, 38, 0.15); - --primary: #dc2626; - --primary-foreground: #ffffff; - - --secondary: #f4f4f5; - --secondary-foreground: #3f3f46; - --accent-2: #0d9488; - --accent-2-muted: rgba(13, 148, 136, 0.75); - --accent-2-subtle: rgba(13, 148, 136, 0.12); - - --ok: #16a34a; - --ok-muted: rgba(22, 163, 74, 0.75); - --ok-subtle: rgba(22, 163, 74, 0.1); - --destructive: #dc2626; - --destructive-foreground: #fafafa; - --warn: #d97706; - --warn-muted: rgba(217, 119, 6, 0.75); - --warn-subtle: rgba(217, 119, 6, 0.1); - --danger: #dc2626; - --danger-muted: rgba(220, 38, 38, 0.75); - --danger-subtle: rgba(220, 38, 38, 0.1); - --info: #2563eb; - - --focus: rgba(220, 38, 38, 0.2); - --focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 16px var(--accent-glow); - - --grid-line: rgba(0, 0, 0, 0.05); - - /* Light shadows */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); - --shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04); - --shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.04); - --shadow-glow: 0 0 24px var(--accent-glow); - + --bg: #f5f1ea; + --bg-accent: #ffffff; + --bg-grad-1: #f1e6d6; + --bg-grad-2: #e5eef4; + --bg-overlay: rgba(28, 32, 46, 0.05); + --bg-glow: rgba(52, 199, 183, 0.14); + --panel: rgba(255, 255, 255, 0.9); + --panel-strong: rgba(255, 255, 255, 0.97); + --chrome: rgba(255, 255, 255, 0.75); + --chrome-strong: rgba(255, 255, 255, 0.88); + --text: rgba(27, 36, 50, 0.98); + --chat-text: rgba(36, 48, 66, 0.9); + --muted: rgba(80, 94, 114, 0.7); + --border: rgba(18, 24, 40, 0.12); + --border-strong: rgba(18, 24, 40, 0.2); + --accent: #e28a3f; + --accent-2: #1ba99d; + --ok: #1aa86c; + --warn: #b3771c; + --danger: #d44848; + --focus: rgba(226, 138, 63, 0.35); + --grid-line: rgba(18, 24, 40, 0.06); color-scheme: light; } @@ -198,19 +70,32 @@ body { body { margin: 0; - font: 400 14px/1.55 var(--font-body); - letter-spacing: -0.02em; - background: var(--bg); + font: 15px/1.5 var(--font-body); + background: + radial-gradient(1200px 900px at 15% -10%, var(--bg-grad-1) 0%, transparent 55%) fixed, + radial-gradient(900px 700px at 80% 10%, var(--bg-grad-2) 0%, transparent 60%) fixed, + linear-gradient(160deg, var(--bg) 0%, var(--bg-accent) 100%) fixed; color: var(--text); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } -/* Theme transition */ +body::before { + content: ""; + position: fixed; + inset: 0; + background: + linear-gradient(140deg, var(--bg-overlay) 0%, rgba(255, 255, 255, 0) 40%), + radial-gradient(620px 420px at 75% 75%, var(--bg-glow), transparent 60%); + pointer-events: none; + z-index: 0; +} + +/* Grid overlay removed for cleaner look */ + @keyframes theme-circle-transition { 0% { clip-path: circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)); } + 100% { clip-path: circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)); } @@ -229,7 +114,7 @@ html.theme-transition::view-transition-old(theme) { html.theme-transition::view-transition-new(theme) { mix-blend-mode: normal; z-index: 2; - animation: theme-circle-transition 0.4s var(--ease-out) forwards; + animation: theme-circle-transition 0.45s ease-out forwards; } @media (prefers-reduced-motion: reduce) { @@ -247,12 +132,7 @@ clawdbot-app { } a { - color: var(--accent); - text-decoration: none; -} - -a:hover { - text-decoration: underline; + color: inherit; } button, @@ -263,35 +143,10 @@ select { color: inherit; } -::selection { - background: var(--accent-subtle); - color: var(--text-strong); -} - -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: var(--radius-full); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--border-strong); -} - -/* Animations - Polished with spring feel */ @keyframes rise { from { opacity: 0; - transform: translateY(8px); + transform: translateY(6px); } to { opacity: 1; @@ -299,26 +154,6 @@ select { } } -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes scale-in { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} - @keyframes dashboard-enter { from { opacity: 0; @@ -329,44 +164,3 @@ select { transform: translateY(0); } } - -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -@keyframes pulse-subtle { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.7; - } -} - -@keyframes glow-pulse { - 0%, 100% { - box-shadow: 0 0 0 rgba(255, 92, 92, 0); - } - 50% { - box-shadow: 0 0 20px var(--accent-glow); - } -} - -/* Stagger animation delays for grouped elements */ -.stagger-1 { animation-delay: 0ms; } -.stagger-2 { animation-delay: 50ms; } -.stagger-3 { animation-delay: 100ms; } -.stagger-4 { animation-delay: 150ms; } -.stagger-5 { animation-delay: 200ms; } -.stagger-6 { animation-delay: 250ms; } - -/* Focus visible styles */ -:focus-visible { - outline: none; - box-shadow: var(--focus-ring); -} diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 39b641826..e91989dfc 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -105,7 +105,9 @@ img.chat-avatar { border-radius: var(--radius-lg); padding: 10px 14px; box-shadow: none; - transition: background 150ms ease-out, border-color 150ms ease-out; + transition: + background 150ms ease-out, + border-color 150ms ease-out; max-width: 100%; word-wrap: break-word; } @@ -128,7 +130,9 @@ img.chat-avatar { cursor: pointer; opacity: 0; pointer-events: none; - transition: opacity 120ms ease-out, background 120ms ease-out; + transition: + opacity 120ms ease-out, + background 120ms ease-out; } .chat-copy-btn__icon { @@ -243,7 +247,8 @@ img.chat-avatar { } @keyframes pulsing-border { - 0%, 100% { + 0%, + 100% { border-color: var(--border); } 50% { diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index e11fedb71..14961596c 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -77,7 +77,10 @@ display: flex; align-items: center; justify-content: center; - transition: background 150ms ease-out, color 150ms ease-out, border-color 150ms ease-out; + transition: + background 150ms ease-out, + color 150ms ease-out, + border-color 150ms ease-out; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } diff --git a/ui/src/styles/chat/tool-cards.css b/ui/src/styles/chat/tool-cards.css index 052e63dbb..85d299b41 100644 --- a/ui/src/styles/chat/tool-cards.css +++ b/ui/src/styles/chat/tool-cards.css @@ -4,17 +4,17 @@ border-radius: 8px; padding: 12px; margin-top: 8px; - background: var(--card); - box-shadow: inset 0 1px 0 var(--card-highlight); - transition: border-color 150ms ease-out, background 150ms ease-out; + transition: + border-color 150ms ease-out, + background 150ms ease-out; /* Fixed max-height to ensure cards don't expand too much */ max-height: 120px; overflow: hidden; } .chat-tool-card:hover { - border-color: var(--border-strong); - background: var(--bg-hover); + border-color: var(--accent); + background: rgba(0, 0, 0, 0.06); } /* First tool card in a group - no top margin */ @@ -52,63 +52,33 @@ display: inline-flex; align-items: center; justify-content: center; - width: 16px; - height: 16px; + width: 18px; + height: 18px; + font-size: 14px; + line-height: 1; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; + vertical-align: middle; flex-shrink: 0; } -.chat-tool-card__icon svg { - width: 14px; - height: 14px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - /* "View >" action link */ .chat-tool-card__action { - display: inline-flex; - align-items: center; - gap: 4px; font-size: 12px; color: var(--accent); opacity: 0.8; transition: opacity 150ms ease-out; } -.chat-tool-card__action svg { - width: 12px; - height: 12px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - .chat-tool-card--clickable:hover .chat-tool-card__action { opacity: 1; } /* Status indicator for completed/empty results */ .chat-tool-card__status { - display: inline-flex; - align-items: center; + font-size: 14px; color: var(--ok); } -.chat-tool-card__status svg { - width: 14px; - height: 14px; - stroke: currentColor; - fill: none; - stroke-width: 2px; - stroke-linecap: round; - stroke-linejoin: round; -} - .chat-tool-card__status-text { font-size: 11px; margin-top: 4px; @@ -126,18 +96,18 @@ color: var(--muted); margin-top: 8px; padding: 8px 10px; - background: var(--secondary); - border-radius: var(--radius-md); + background: rgba(0, 0, 0, 0.08); + border-radius: 6px; white-space: pre-wrap; overflow: hidden; max-height: 44px; line-height: 1.4; - border: 1px solid var(--border); + border: 1px solid rgba(255, 255, 255, 0.04); } .chat-tool-card--clickable:hover .chat-tool-card__preview { - background: var(--bg-hover); - border-color: var(--border-strong); + background: rgba(0, 0, 0, 0.12); + border-color: rgba(255, 255, 255, 0.08); } /* Short inline output */ @@ -146,8 +116,8 @@ color: var(--text); margin-top: 6px; padding: 6px 8px; - background: var(--secondary); - border-radius: var(--radius-sm); + background: rgba(0, 0, 0, 0.06); + border-radius: 4px; white-space: pre-wrap; word-break: break-word; } @@ -187,7 +157,9 @@ } @keyframes reading-pulse { - 0%, 60%, 100% { + 0%, + 60%, + 100% { opacity: 0.3; transform: scale(0.8); } diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 27dfe62d1..fc9e06731 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1,75 +1,44 @@ -@import './chat.css'; - -/* =========================================== - Cards - Refined with depth - =========================================== */ +@import "./chat.css"; .card { border: 1px solid var(--border); - background: var(--card); - border-radius: var(--radius-lg); - padding: 20px; - animation: rise 0.35s var(--ease-out) backwards; - transition: - border-color var(--duration-normal) var(--ease-out), - box-shadow var(--duration-normal) var(--ease-out), - transform var(--duration-normal) var(--ease-out); - box-shadow: var(--shadow-sm), inset 0 1px 0 var(--card-highlight); -} - -.card:hover { - border-color: var(--border-strong); - box-shadow: var(--shadow-md), inset 0 1px 0 var(--card-highlight); + background: linear-gradient(160deg, rgba(255, 255, 255, 0.04), transparent 65%), var(--panel); + border-radius: 16px; + padding: 16px; + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28); + animation: rise 0.4s ease; } .card-title { - font-size: 15px; - font-weight: 600; - letter-spacing: -0.02em; - color: var(--text-strong); + font-family: var(--font-display); + font-size: 16px; + letter-spacing: 0.6px; + text-transform: uppercase; } .card-sub { color: var(--muted); - font-size: 13px; - margin-top: 6px; - line-height: 1.5; + font-size: 12px; } -/* =========================================== - Stats - Bold values, subtle labels - =========================================== */ - .stat { - background: var(--card); - border-radius: var(--radius-md); - padding: 14px 16px; - border: 1px solid var(--border); - 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); -} - -.stat:hover { - border-color: var(--border-strong); - box-shadow: var(--shadow-sm), inset 0 1px 0 var(--card-highlight); + background: + linear-gradient(140deg, rgba(255, 255, 255, 0.04), transparent 70%), var(--panel-strong); + border-radius: 14px; + padding: 12px; + border: 1px solid var(--border-strong); } .stat-label { color: var(--muted); font-size: 11px; - font-weight: 500; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 1px; } .stat-value { - font-size: 24px; - font-weight: 700; + font-size: 18px; margin-top: 6px; - letter-spacing: -0.03em; - line-height: 1.1; } .stat-value.ok { @@ -87,13 +56,9 @@ .note-title { font-weight: 600; - letter-spacing: -0.01em; + letter-spacing: 0.2px; } -/* =========================================== - Status List - =========================================== */ - .status-list { display: grid; gap: 8px; @@ -103,8 +68,8 @@ display: flex; justify-content: space-between; gap: 12px; - padding: 8px 0; - border-bottom: 1px solid var(--border); + padding: 6px 0; + border-bottom: 1px dashed rgba(255, 255, 255, 0.06); } .status-list div:last-child { @@ -112,28 +77,25 @@ } .account-count { - margin-top: 10px; + margin-top: 8px; font-size: 12px; - font-weight: 500; + font-weight: 600; + letter-spacing: 0.4px; color: var(--muted); } .account-card-list { margin-top: 16px; display: grid; - gap: 12px; + gap: 10px; } .account-card { border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: 10px; padding: 12px; - background: var(--bg-elevated); - transition: border-color var(--duration-fast) ease; -} - -.account-card:hover { - border-color: var(--border-strong); + background: + linear-gradient(160deg, rgba(255, 255, 255, 0.06), transparent), rgba(255, 255, 255, 0.03); } .account-card-header { @@ -144,7 +106,7 @@ } .account-card-title { - font-weight: 500; + font-weight: 600; } .account-card-id { @@ -154,7 +116,7 @@ } .account-card-status { - margin-top: 10px; + margin-top: 8px; font-size: 13px; } @@ -163,52 +125,32 @@ } .account-card-error { - margin-top: 8px; + margin-top: 6px; color: var(--danger); font-size: 12px; } -/* =========================================== - Labels & Pills - =========================================== */ - .label { color: var(--muted); - font-size: 12px; - font-weight: 500; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.9px; } .pill { display: inline-flex; align-items: center; - gap: 6px; - border: 1px solid var(--border); + gap: 8px; + border: 1px solid var(--border-strong); padding: 6px 12px; - border-radius: var(--radius-full); - background: var(--secondary); - font-size: 13px; - font-weight: 500; - transition: border-color var(--duration-fast) ease; + border-radius: 999px; + background: linear-gradient(160deg, rgba(255, 255, 255, 0.06), transparent), var(--panel); } -.pill:hover { - border-color: var(--border-strong); -} - -.pill.danger { - border-color: var(--danger-subtle); - background: var(--danger-subtle); - color: var(--danger); -} - -/* =========================================== - Theme Toggle - =========================================== */ - .theme-toggle { --theme-item: 28px; - --theme-gap: 2px; - --theme-pad: 4px; + --theme-gap: 6px; + --theme-pad: 6px; position: relative; } @@ -218,9 +160,9 @@ grid-template-columns: repeat(3, var(--theme-item)); gap: var(--theme-gap); padding: var(--theme-pad); - border-radius: var(--radius-full); - border: 1px solid var(--border); - background: var(--secondary); + border-radius: 999px; + border: 1px solid var(--border-strong); + background: rgba(255, 255, 255, 0.04); } .theme-toggle__indicator { @@ -229,11 +171,16 @@ left: var(--theme-pad); width: var(--theme-item); height: var(--theme-item); - border-radius: var(--radius-full); + border-radius: 999px; transform: translateY(-50%) translateX(calc(var(--theme-index, 0) * (var(--theme-item) + var(--theme-gap)))); - background: var(--accent); - transition: transform var(--duration-normal) var(--ease-out); + background: linear-gradient(160deg, rgba(255, 255, 255, 0.12), transparent), var(--panel-strong); + border: 1px solid var(--border-strong); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.25); + transition: + transform 180ms ease-out, + background 180ms ease-out, + box-shadow 180ms ease-out; z-index: 0; } @@ -243,176 +190,99 @@ display: grid; place-items: center; border: 0; - border-radius: var(--radius-full); + border-radius: 999px; background: transparent; color: var(--muted); cursor: pointer; position: relative; z-index: 1; - transition: color var(--duration-fast) ease; + transition: + color 150ms ease-out, + background 150ms ease-out; } .theme-toggle__button:hover { color: var(--text); + background: rgba(255, 255, 255, 0.08); } .theme-toggle__button.active { - color: var(--accent-foreground); -} - -.theme-toggle__button.active .theme-icon { - stroke: var(--accent-foreground); + color: var(--text); } .theme-icon { - width: 14px; - height: 14px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -/* =========================================== - Status Dot - With glow for emphasis - =========================================== */ - -.statusDot { - width: 8px; - height: 8px; - border-radius: var(--radius-full); - background: var(--danger); - box-shadow: 0 0 8px rgba(239, 68, 68, 0.5); - animation: pulse-subtle 2s ease-in-out infinite; -} - -.statusDot.ok { - background: var(--ok); - box-shadow: 0 0 8px rgba(34, 197, 94, 0.5); - animation: none; -} - -/* =========================================== - Buttons - Tactile with personality - =========================================== */ - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - border: 1px solid var(--border); - background: var(--bg-elevated); - padding: 9px 16px; - border-radius: var(--radius-md); - font-size: 13px; - font-weight: 500; - letter-spacing: -0.01em; - cursor: pointer; - transition: - border-color var(--duration-fast) var(--ease-out), - background var(--duration-fast) var(--ease-out), - box-shadow var(--duration-fast) var(--ease-out), - transform var(--duration-fast) var(--ease-out); -} - -.btn:hover { - background: var(--bg-hover); - border-color: var(--border-strong); - transform: translateY(-1px); - box-shadow: var(--shadow-sm); -} - -.btn:active { - background: var(--secondary); - transform: translateY(0); - box-shadow: none; -} - -.btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; - stroke-width: 1.5px; + stroke-width: 1.75px; stroke-linecap: round; stroke-linejoin: round; - flex-shrink: 0; } -.btn.primary { - border-color: var(--accent); - background: var(--accent); - color: var(--primary-foreground); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); -} - -.btn.primary:hover { - background: var(--accent-hover); - border-color: var(--accent-hover); - box-shadow: var(--shadow-md), 0 0 20px var(--accent-glow); -} - -/* Keyboard shortcut badge (shadcn style) */ -.btn-kbd { - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 6px; - padding: 2px 5px; - font-family: var(--mono); - font-size: 11px; - font-weight: 500; - line-height: 1; - border-radius: 4px; - background: rgba(255, 255, 255, 0.15); - color: inherit; - opacity: 0.8; -} - -.btn.primary .btn-kbd { - background: rgba(255, 255, 255, 0.2); -} - -:root[data-theme="light"] .btn-kbd { - background: rgba(0, 0, 0, 0.08); -} - -:root[data-theme="light"] .btn.primary .btn-kbd { - background: rgba(255, 255, 255, 0.25); -} - -.btn.active { - border-color: var(--accent); - background: var(--accent-subtle); - color: var(--accent); -} - -.btn.danger { - border-color: transparent; - background: var(--danger-subtle); +.pill.danger { + border-color: rgba(255, 92, 92, 0.5); color: var(--danger); } -.btn.danger:hover { - background: rgba(239, 68, 68, 0.15); +.statusDot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--danger); + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25); +} + +.statusDot.ok { + background: var(--ok); + box-shadow: + 0 0 0 2px rgba(0, 0, 0, 0.25), + 0 0 10px rgba(43, 217, 127, 0.4); +} + +.btn { + border: 1px solid var(--border-strong); + background: rgba(255, 255, 255, 0.04); + padding: 8px 14px; + border-radius: 999px; + cursor: pointer; + transition: + transform 150ms ease, + border-color 150ms ease, + background 150ms ease; +} + +.btn:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); +} + +.btn.primary { + border-color: rgba(245, 159, 74, 0.45); + background: rgba(245, 159, 74, 0.2); +} + +.btn.active { + border-color: rgba(245, 159, 74, 0.55); + background: rgba(245, 159, 74, 0.16); +} + +.btn.danger { + border-color: rgba(255, 107, 107, 0.45); + background: rgba(255, 107, 107, 0.18); } .btn--sm { - padding: 6px 10px; + padding: 5px 10px; font-size: 12px; } .btn:disabled { opacity: 0.5; cursor: not-allowed; + transform: none; } -/* =========================================== - Form Fields - =========================================== */ - .field { display: grid; gap: 6px; @@ -424,46 +294,61 @@ .field span { color: var(--muted); - font-size: 13px; - font-weight: 500; + font-size: 11px; + letter-spacing: 0.4px; } .field input, .field textarea, .field select { - border: 1px solid var(--input); - background: var(--card); - border-radius: var(--radius-md); - padding: 8px 12px; + border: 1px solid var(--border-strong); + background: rgba(0, 0, 0, 0.22); + border-radius: 12px; + padding: 9px 11px; outline: none; - box-shadow: inset 0 1px 0 var(--card-highlight); transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; + border-color 150ms ease, + box-shadow 150ms ease, + background 150ms ease; } .field input:focus, .field textarea:focus, .field select:focus { - border-color: var(--ring); - box-shadow: var(--focus-ring); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--focus); + background: rgba(0, 0, 0, 0.28); } .field select { appearance: none; - padding-right: 36px; - 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"); + padding-right: 38px; + background-color: var(--panel-strong); + background-image: + linear-gradient(45deg, transparent 50%, var(--muted) 50%), + linear-gradient(135deg, var(--muted) 50%, transparent 50%), + linear-gradient(to right, transparent, transparent); + background-position: + calc(100% - 18px) 50%, + calc(100% - 12px) 50%, + calc(100% - 38px) 50%; + background-size: + 6px 6px, + 6px 6px, + 1px 60%; background-repeat: no-repeat; - background-position: right 10px center; - cursor: pointer; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); } .field textarea { font-family: var(--mono); - min-height: 160px; + min-height: 180px; resize: vertical; white-space: pre; - line-height: 1.5; +} + +.field textarea:focus { + background: rgba(0, 0, 0, 0.32); } .field.checkbox { @@ -478,9 +363,6 @@ .config-form .field.checkbox input[type="checkbox"] { margin: 0; - width: 16px; - height: 16px; - accent-color: var(--accent); } .form-grid { @@ -492,27 +374,36 @@ :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); + background: rgba(255, 255, 255, 1); + border-color: rgba(16, 24, 40, 0.25); + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06); } +:root[data-theme="light"] .field input:focus, +:root[data-theme="light"] .field textarea:focus, +:root[data-theme="light"] .field select:focus { + background: #ffffff; +} + +/* Light theme button overrides */ :root[data-theme="light"] .btn { - background: var(--bg); - border-color: var(--input); + background: rgba(255, 255, 255, 0.9); + border-color: rgba(16, 24, 40, 0.2); + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); } :root[data-theme="light"] .btn:hover { - background: var(--bg-hover); + background: rgba(255, 255, 255, 1); + border-color: rgba(16, 24, 40, 0.3); } :root[data-theme="light"] .btn.primary { - background: var(--accent); - border-color: var(--accent); + background: rgba(245, 159, 74, 0.15); } -/* =========================================== - Utilities - =========================================== */ +:root[data-theme="light"] .btn.active { + background: rgba(245, 159, 74, 0.12); +} .muted { color: var(--muted); @@ -522,44 +413,34 @@ font-family: var(--mono); } -/* =========================================== - Callouts - Informative with subtle depth - =========================================== */ - .callout { - padding: 14px 16px; - border-radius: var(--radius-md); - background: var(--secondary); + padding: 10px 12px; + border-radius: 14px; + background: + linear-gradient(160deg, rgba(255, 255, 255, 0.06), transparent), rgba(255, 255, 255, 0.03); border: 1px solid var(--border); - font-size: 13px; - line-height: 1.5; - position: relative; } .callout.danger { - border-color: rgba(239, 68, 68, 0.25); - background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(239, 68, 68, 0.04) 100%); + border-color: rgba(255, 92, 92, 0.4); color: var(--danger); } .callout.info { - border-color: rgba(59, 130, 246, 0.25); - background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.04) 100%); - color: var(--info); + border-color: rgba(92, 156, 255, 0.4); + color: var(--accent); } .callout.success { - border-color: rgba(34, 197, 94, 0.25); - background: linear-gradient(135deg, rgba(34, 197, 94, 0.08) 0%, rgba(34, 197, 94, 0.04) 100%); - color: var(--ok); + border-color: rgba(92, 255, 128, 0.4); + color: var(--positive, #5cff80); } -/* Compaction indicator */ .compaction-indicator { font-size: 13px; - padding: 10px 12px; + padding: 8px 12px; margin-bottom: 8px; - animation: fade-in 0.2s var(--ease-out); + animation: compaction-fade-in 0.2s ease-out; } .compaction-indicator--active { @@ -567,11 +448,23 @@ } .compaction-indicator--complete { - animation: fade-in 0.2s var(--ease-out); + animation: compaction-fade-in 0.2s ease-out; +} + +@keyframes compaction-fade-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes compaction-pulse { - 0%, 100% { + 0%, + 100% { opacity: 0.7; } 50% { @@ -579,54 +472,46 @@ } } -/* =========================================== - Code Blocks - =========================================== */ - .code-block { font-family: var(--mono); - font-size: 13px; - line-height: 1.5; - background: var(--secondary); - padding: 12px; - border-radius: var(--radius-md); + font-size: 12px; + background: rgba(0, 0, 0, 0.35); + padding: 10px; + border-radius: 12px; border: 1px solid var(--border); max-height: 360px; overflow: auto; - max-width: 100%; } :root[data-theme="light"] .code-block, :root[data-theme="light"] .list-item, :root[data-theme="light"] .table-row, :root[data-theme="light"] .chip { - background: var(--bg); + background: rgba(255, 255, 255, 0.85); } -/* =========================================== - Lists - =========================================== */ - .list { display: grid; - gap: 8px; + gap: 12px; container-type: inline-size; } .list-item { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(200px, 260px); - gap: 16px; + grid-template-columns: minmax(0, 1fr) minmax(220px, 260px); + gap: 14px; align-items: start; border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: 14px; padding: 12px; - background: var(--card); - transition: border-color var(--duration-fast) ease; + background: rgba(0, 0, 0, 0.2); } .list-item-clickable { cursor: pointer; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; } .list-item-clickable:hover { @@ -635,17 +520,17 @@ .list-item-selected { border-color: var(--accent); - box-shadow: var(--focus-ring); + box-shadow: 0 0 0 1px var(--focus); } .list-main { display: grid; - gap: 4px; + gap: 6px; min-width: 0; } .list-title { - font-weight: 500; + font-weight: 600; } .list-sub { @@ -656,10 +541,10 @@ .list-meta { text-align: right; color: var(--muted); - font-size: 12px; + font-size: 11px; display: grid; gap: 4px; - min-width: 200px; + min-width: 220px; } .list-meta .btn { @@ -683,33 +568,19 @@ } } -/* =========================================== - Chips - Compact and punchy - =========================================== */ - .chip-row { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 6px; } .chip { - font-size: 12px; - font-weight: 500; + font-size: 11px; border: 1px solid var(--border); - border-radius: var(--radius-full); - padding: 5px 12px; + border-radius: 999px; + padding: 4px 8px; color: var(--muted); - background: var(--secondary); - transition: - border-color var(--duration-fast) var(--ease-out), - background var(--duration-fast) var(--ease-out), - transform var(--duration-fast) var(--ease-out); -} - -.chip:hover { - border-color: var(--border-strong); - transform: translateY(-1px); + background: rgba(0, 0, 0, 0.2); } .chip input { @@ -718,23 +589,17 @@ .chip-ok { color: var(--ok); - border-color: rgba(34, 197, 94, 0.3); - background: var(--ok-subtle); + border-color: rgba(27, 217, 138, 0.4); } .chip-warn { color: var(--warn); - border-color: rgba(245, 158, 11, 0.3); - background: var(--warn-subtle); + border-color: rgba(242, 201, 76, 0.4); } -/* =========================================== - Tables - =========================================== */ - .table { display: grid; - gap: 6px; + gap: 8px; } .table-head, @@ -746,43 +611,33 @@ } .table-head { - font-size: 12px; - font-weight: 500; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.8px; color: var(--muted); - padding: 0 12px; } .table-row { border: 1px solid var(--border); - padding: 10px 12px; - border-radius: var(--radius-md); - background: var(--card); - transition: border-color var(--duration-fast) ease; -} - -.table-row:hover { - border-color: var(--border-strong); + padding: 10px; + border-radius: 12px; + background: rgba(0, 0, 0, 0.2); } .session-link { text-decoration: none; color: var(--accent); - font-weight: 500; } .session-link:hover { text-decoration: underline; } -/* =========================================== - Log Stream - =========================================== */ - .log-stream { border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--card); - max-height: 500px; + border-radius: 14px; + background: rgba(0, 0, 0, 0.2); + max-height: 520px; overflow: auto; container-type: inline-size; } @@ -792,14 +647,9 @@ grid-template-columns: 90px 70px minmax(140px, 200px) minmax(0, 1fr); gap: 12px; align-items: start; - padding: 8px 12px; + padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 12px; - transition: background var(--duration-fast) ease; -} - -.log-row:hover { - background: var(--bg-hover); } .log-row:last-child { @@ -808,14 +658,14 @@ .log-time { color: var(--muted); - font-family: var(--mono); } .log-level { - font-size: 11px; - font-weight: 500; + text-transform: uppercase; + font-size: 10px; + font-weight: 600; border: 1px solid var(--border); - border-radius: var(--radius-sm); + border-radius: 999px; padding: 2px 6px; width: fit-content; } @@ -827,18 +677,18 @@ .log-level.info { color: var(--info); - border-color: rgba(59, 130, 246, 0.3); + border-color: rgba(76, 150, 242, 0.4); } .log-level.warn { color: var(--warn); - border-color: var(--warn-subtle); + border-color: rgba(242, 201, 76, 0.4); } .log-level.error, .log-level.fatal { color: var(--danger); - border-color: var(--danger-subtle); + border-color: rgba(255, 92, 92, 0.4); } .log-chip.trace, @@ -848,29 +698,27 @@ .log-chip.info { color: var(--info); - border-color: rgba(59, 130, 246, 0.3); + border-color: rgba(76, 150, 242, 0.4); } .log-chip.warn { color: var(--warn); - border-color: var(--warn-subtle); + border-color: rgba(242, 201, 76, 0.4); } .log-chip.error, .log-chip.fatal { color: var(--danger); - border-color: var(--danger-subtle); + border-color: rgba(255, 92, 92, 0.4); } .log-subsystem { color: var(--muted); - font-family: var(--mono); } .log-message { white-space: pre-wrap; word-break: break-word; - font-family: var(--mono); } @container (max-width: 620px) { @@ -883,10 +731,6 @@ } } -/* =========================================== - Chat - =========================================== */ - .chat { display: flex; flex-direction: column; @@ -901,7 +745,7 @@ display: flex; justify-content: space-between; align-items: flex-end; - gap: 16px; + gap: 12px; flex-wrap: wrap; } @@ -916,7 +760,7 @@ .chat-header__right { display: flex; align-items: center; - gap: 8px; + gap: 10px; } .chat-session { @@ -924,36 +768,42 @@ } .chat-thread { - margin-top: 16px; + margin-top: 12px; display: flex; flex-direction: column; gap: 12px; flex: 1; - min-height: 0; - overflow-y: auto; + min-height: 0; /* Allow flex shrinking for scroll behavior */ + overflow-y: auto; /* Enable scrolling */ overflow-x: hidden; - padding: 16px 12px; + padding: 14px 12px; min-width: 0; border-radius: 0; border: none; background: transparent; } -/* Chat queue */ +:root[data-theme="light"] .chat-thread { + background: transparent; +} + .chat-queue { margin-top: 12px; - padding: 12px; - border-radius: var(--radius-lg); + padding: 10px 12px; + border-radius: 16px; border: 1px solid var(--border); - background: var(--card); + background: rgba(0, 0, 0, 0.18); display: grid; gap: 8px; } +:root[data-theme="light"] .chat-queue { + background: rgba(16, 24, 40, 0.04); +} + .chat-queue__title { - font-family: var(--mono); + font-family: var(--font-mono); font-size: 12px; - font-weight: 500; color: var(--muted); } @@ -966,17 +816,21 @@ display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; - gap: 12px; - padding: 10px 12px; - border-radius: var(--radius-md); - border: 1px dashed var(--border-strong); - background: var(--secondary); + gap: 10px; + padding: 8px 10px; + border-radius: 12px; + border: 1px dashed var(--border); + background: rgba(0, 0, 0, 0.2); +} + +:root[data-theme="light"] .chat-queue__item { + background: rgba(16, 24, 40, 0.05); } .chat-queue__text { color: var(--chat-text); font-size: 13px; - line-height: 1.45; + line-height: 1.4; white-space: pre-wrap; overflow: hidden; display: -webkit-box; @@ -991,7 +845,6 @@ line-height: 1; } -/* Chat lines */ .chat-line { display: flex; } @@ -1008,119 +861,122 @@ .chat-msg { display: grid; gap: 6px; - max-width: min(700px, 82%); + max-width: min(720px, 82%); } .chat-line.user .chat-msg { justify-items: end; } -/* Chat bubbles */ .chat-bubble { - border: 1px solid transparent; - background: var(--card); - border-radius: var(--radius-lg); - padding: 10px 14px; + border: 1px solid var(--border); + background: rgba(0, 0, 0, 0.24); + border-radius: 16px; + padding: 10px 12px; min-width: 0; + box-shadow: 0 12px 22px rgba(0, 0, 0, 0.24); } :root[data-theme="light"] .chat-bubble { - border-color: var(--border); - background: var(--bg); + background: rgba(255, 255, 255, 0.85); + box-shadow: 0 12px 26px rgba(16, 24, 40, 0.08); } .chat-line.user .chat-bubble { - border-color: transparent; - background: var(--accent-subtle); -} - -:root[data-theme="light"] .chat-line.user .chat-bubble { - border-color: rgba(234, 88, 12, 0.2); - background: rgba(251, 146, 60, 0.12); + border-color: rgba(245, 159, 74, 0.45); + background: linear-gradient(135deg, rgba(245, 159, 74, 0.26) 0%, rgba(245, 159, 74, 0.12) 100%); } .chat-line.assistant .chat-bubble { - border-color: transparent; - background: var(--secondary); + border-color: rgba(52, 199, 183, 0.2); + background: linear-gradient(135deg, rgba(52, 199, 183, 0.12) 0%, rgba(0, 0, 0, 0.24) 100%); } :root[data-theme="light"] .chat-line.assistant .chat-bubble { - border-color: var(--border); - background: var(--bg-muted); + background: linear-gradient(135deg, rgba(27, 185, 177, 0.12) 0%, rgba(255, 255, 255, 0.85) 100%); } @keyframes chatStreamPulse { - 0%, 100% { - border-color: var(--border); + 0% { + box-shadow: + 0 12px 22px rgba(0, 0, 0, 0.24), + 0 0 0 0 rgba(52, 199, 183, 0); } - 50% { - border-color: var(--accent); + 60% { + box-shadow: + 0 12px 22px rgba(0, 0, 0, 0.24), + 0 0 0 6px rgba(52, 199, 183, 0.08); + } + 100% { + box-shadow: + 0 12px 22px rgba(0, 0, 0, 0.24), + 0 0 0 0 rgba(52, 199, 183, 0); } } .chat-bubble.streaming { - animation: chatStreamPulse 1.5s ease-in-out infinite; + border-color: rgba(52, 199, 183, 0.4); + animation: chatStreamPulse 1.6s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .chat-bubble.streaming { animation: none; - border-color: var(--accent); } } -/* Reading indicator */ .chat-bubble.chat-reading-indicator { width: fit-content; - padding: 10px 16px; + padding: 10px 14px; } .chat-reading-indicator__dots { display: inline-flex; align-items: center; - gap: 4px; - height: 12px; + gap: 6px; + height: 10px; } .chat-reading-indicator__dots > span { display: inline-block; width: 6px; height: 6px; - border-radius: var(--radius-full); - background: var(--muted); - opacity: 0.6; + border-radius: 999px; + background: var(--chat-text); + opacity: 0.55; transform: translateY(0); - animation: chatReadingDot 1.2s ease-in-out infinite; + animation: chatReadingDot 1.1s ease-in-out infinite; will-change: transform, opacity; } .chat-reading-indicator__dots > span:nth-child(2) { - animation-delay: 0.15s; + animation-delay: 0.12s; } .chat-reading-indicator__dots > span:nth-child(3) { - animation-delay: 0.3s; + animation-delay: 0.24s; } @keyframes chatReadingDot { - 0%, 80%, 100% { - opacity: 0.4; - transform: translateY(0); + 0%, + 80%, + 100% { + opacity: 0.38; + transform: translateY(0) scale(0.92); } 40% { opacity: 1; - transform: translateY(-3px); + transform: translateY(-3px) scale(1.18); } } @media (prefers-reduced-motion: reduce) { .chat-reading-indicator__dots > span { animation: none; - opacity: 0.6; + opacity: 0.75; } } -/* Chat text */ .chat-text { overflow-wrap: anywhere; word-break: break-word; @@ -1137,7 +993,7 @@ } .chat-text :where(ul, ol) { - padding-left: 1.2em; + padding-left: 1.1em; } .chat-text :where(li + li) { @@ -1146,51 +1002,58 @@ .chat-text :where(a) { color: var(--accent); + text-decoration-thickness: 2px; + text-underline-offset: 2px; } .chat-text :where(a:hover) { - text-decoration: underline; + text-decoration-thickness: 3px; } .chat-text :where(blockquote) { - border-left: 2px solid var(--border-strong); + border-left: 2px solid rgba(255, 255, 255, 0.14); padding-left: 12px; color: var(--muted); } +:root[data-theme="light"] .chat-text :where(blockquote) { + border-left-color: rgba(16, 24, 40, 0.16); +} + .chat-text :where(hr) { border: 0; border-top: 1px solid var(--border); - margin: 1em 0; + opacity: 0.6; + margin: 0.9em 0; } .chat-text :where(code) { - font-family: var(--mono); - font-size: 0.9em; + font-family: var(--font-mono); + font-size: 0.92em; } .chat-text :where(:not(pre) > code) { padding: 0.15em 0.35em; - border-radius: var(--radius-sm); + border-radius: 8px; border: 1px solid var(--border); - background: var(--secondary); + background: rgba(0, 0, 0, 0.2); } :root[data-theme="light"] .chat-text :where(:not(pre) > code) { - background: var(--bg-muted); + background: rgba(16, 24, 40, 0.05); } .chat-text :where(pre) { margin-top: 0.75em; padding: 10px 12px; - border-radius: var(--radius-md); + border-radius: 14px; border: 1px solid var(--border); - background: var(--secondary); + background: rgba(0, 0, 0, 0.22); overflow: auto; } :root[data-theme="light"] .chat-text :where(pre) { - background: var(--bg-muted); + background: rgba(16, 24, 40, 0.04); } .chat-text :where(pre code) { @@ -1202,46 +1065,43 @@ margin-top: 0.75em; border-collapse: collapse; width: 100%; - font-size: 13px; + font-size: 12px; } .chat-text :where(th, td) { border: 1px solid var(--border); - padding: 6px 10px; + padding: 6px 8px; vertical-align: top; } .chat-text :where(th) { - font-family: var(--mono); - font-weight: 500; + font-family: var(--font-mono); + font-weight: 600; color: var(--muted); - background: var(--secondary); } -/* Tool cards */ .chat-tool-card { margin-top: 8px; - padding: 10px 12px; - border-radius: var(--radius-md); + padding: 8px 10px; + border-radius: 12px; border: 1px solid var(--border); - background: var(--secondary); + background: rgba(0, 0, 0, 0.22); display: grid; gap: 4px; } :root[data-theme="light"] .chat-tool-card { - background: var(--bg-muted); + background: rgba(255, 255, 255, 0.7); } .chat-tool-card__title { - font-family: var(--mono); + font-family: var(--font-mono); font-size: 12px; - font-weight: 500; - color: var(--text); + color: var(--chat-text); } .chat-tool-card__detail { - font-family: var(--mono); + font-family: var(--font-mono); font-size: 11px; color: var(--muted); } @@ -1251,7 +1111,7 @@ } .chat-tool-card__summary { - font-family: var(--mono); + font-family: var(--font-mono); font-size: 11px; color: var(--muted); cursor: pointer; @@ -1267,28 +1127,28 @@ .chat-tool-card__summary-meta { color: var(--muted); - opacity: 0.7; + opacity: 0.8; } .chat-tool-card__details[open] .chat-tool-card__summary { - color: var(--text); + color: var(--chat-text); } .chat-tool-card__output { - margin-top: 8px; - font-family: var(--mono); + margin-top: 6px; + font-family: var(--font-mono); font-size: 11px; - line-height: 1.5; + line-height: 1.45; white-space: pre-wrap; color: var(--chat-text); - padding: 8px 10px; - border-radius: var(--radius-md); + padding: 8px; + border-radius: 10px; border: 1px solid var(--border); - background: var(--card); + background: rgba(0, 0, 0, 0.2); } :root[data-theme="light"] .chat-tool-card__output { - background: var(--bg); + background: rgba(16, 24, 40, 0.05); } .chat-stamp { @@ -1300,7 +1160,6 @@ text-align: right; } -/* Chat compose */ .chat-compose { margin-top: 12px; display: flex; @@ -1314,14 +1173,14 @@ z-index: 5; margin-top: 0; padding-top: 12px; - background: linear-gradient(180deg, transparent 0%, var(--bg) 40%); + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, var(--panel) 35%); } .shell--chat-focus .chat-compose { bottom: calc(var(--shell-pad) + 8px); - padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); - border-bottom-left-radius: var(--radius-lg); - border-bottom-right-radius: var(--radius-lg); + padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px)); + border-bottom-left-radius: 18px; + border-bottom-right-radius: 18px; } .chat-compose__field { @@ -1330,27 +1189,16 @@ .chat-compose__field textarea { min-height: 72px; - padding: 10px 14px; - border-radius: var(--radius-lg); + padding: 10px 12px; + border-radius: 16px; resize: vertical; white-space: pre-wrap; font-family: var(--font-body); - line-height: 1.5; - border: 1px solid var(--input); - background: var(--card); - box-shadow: inset 0 1px 0 var(--card-highlight); - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; -} - -.chat-compose__field textarea:focus { - border-color: var(--ring); - box-shadow: var(--focus-ring); + line-height: 1.45; } .chat-compose__field textarea:disabled { - opacity: 0.5; + opacity: 0.7; cursor: not-allowed; } @@ -1361,7 +1209,7 @@ @media (max-width: 900px) { .chat-session { - min-width: 180px; + min-width: 200px; } .chat-compose { @@ -1369,35 +1217,27 @@ } } -/* =========================================== - QR Code - =========================================== */ - .qr-wrap { - margin-top: 16px; - border-radius: var(--radius-md); - background: var(--card); - border: 1px dashed var(--border-strong); - padding: 16px; + margin-top: 12px; + border-radius: 14px; + background: rgba(0, 0, 0, 0.2); + border: 1px dashed rgba(255, 255, 255, 0.18); + padding: 12px; display: inline-flex; } .qr-wrap img { - width: 160px; - height: 160px; - border-radius: var(--radius-sm); + width: 180px; + height: 180px; + border-radius: 10px; image-rendering: pixelated; } -/* =========================================== - Exec Approval Modal - =========================================== */ - .exec-approval-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(4px); + background: rgba(8, 12, 18, 0.7); + backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; @@ -1406,58 +1246,59 @@ } .exec-approval-card { - width: min(540px, 100%); - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); + width: min(560px, 100%); + background: var(--panel-strong); + border: 1px solid var(--border-strong); + border-radius: 18px; padding: 20px; - animation: scale-in 0.2s var(--ease-out); + box-shadow: 0 28px 60px rgba(0, 0, 0, 0.35); + animation: rise 0.25s ease; } .exec-approval-header { display: flex; align-items: center; justify-content: space-between; - gap: 16px; + gap: 12px; } .exec-approval-title { + font-family: var(--font-display); font-size: 14px; - font-weight: 600; + letter-spacing: 0.8px; + text-transform: uppercase; } .exec-approval-sub { color: var(--muted); - font-size: 13px; - margin-top: 4px; + font-size: 12px; } .exec-approval-queue { font-size: 11px; - font-weight: 500; + text-transform: uppercase; + letter-spacing: 1px; color: var(--muted); border: 1px solid var(--border); - border-radius: var(--radius-full); + border-radius: 999px; padding: 4px 10px; } .exec-approval-command { margin-top: 12px; padding: 10px 12px; - background: var(--secondary); + background: rgba(0, 0, 0, 0.25); border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: 12px; word-break: break-word; white-space: pre-wrap; - font-family: var(--mono); - font-size: 13px; } .exec-approval-meta { margin-top: 12px; display: grid; gap: 6px; - font-size: 13px; + font-size: 12px; color: var(--muted); } @@ -1474,7 +1315,7 @@ .exec-approval-error { margin-top: 10px; - font-size: 13px; + font-size: 12px; color: var(--danger); } @@ -1482,5 +1323,5 @@ margin-top: 16px; display: flex; flex-wrap: wrap; - gap: 8px; + gap: 10px; } diff --git a/ui/src/styles/config.css b/ui/src/styles/config.css index 7d96ac13f..fd9678acb 100644 --- a/ui/src/styles/config.css +++ b/ui/src/styles/config.css @@ -1,15 +1,15 @@ /* =========================================== - Config Page - Carbon Design System + Config Page - Modern Layout =========================================== */ /* Layout Container */ .config-layout { display: grid; - grid-template-columns: 260px minmax(0, 1fr); + grid-template-columns: 240px minmax(0, 1fr); gap: 0; - height: calc(100vh - 160px); + min-height: calc(100vh - 140px); margin: -16px; - border-radius: var(--radius-xl); + border-radius: 16px; overflow: hidden; border: 1px solid var(--border); background: var(--panel); @@ -18,50 +18,47 @@ /* =========================================== Sidebar =========================================== */ - .config-sidebar { display: flex; flex-direction: column; - background: var(--bg-accent); + background: rgba(0, 0, 0, 0.2); border-right: 1px solid var(--border); - min-height: 0; - overflow: hidden; } :root[data-theme="light"] .config-sidebar { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.03); } .config-sidebar__header { display: flex; align-items: center; justify-content: space-between; - padding: 18px 18px; + padding: 16px; border-bottom: 1px solid var(--border); } .config-sidebar__title { font-weight: 600; font-size: 14px; - letter-spacing: -0.01em; + letter-spacing: 0.3px; } .config-sidebar__footer { margin-top: auto; - padding: 14px; + padding: 12px; border-top: 1px solid var(--border); } /* Search */ .config-search { position: relative; - padding: 14px; + padding: 12px; border-bottom: 1px solid var(--border); } .config-search__icon { position: absolute; - left: 28px; + left: 24px; top: 50%; transform: translateY(-50%); width: 16px; @@ -72,16 +69,16 @@ .config-search__input { width: 100%; - padding: 11px 36px 11px 42px; + padding: 10px 32px 10px 40px; border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); + border-radius: 8px; + background: rgba(0, 0, 0, 0.15); font-size: 13px; outline: none; transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - background var(--duration-fast) ease; + border-color 150ms ease, + box-shadow 150ms ease, + background 150ms ease; } .config-search__input::placeholder { @@ -90,42 +87,42 @@ .config-search__input:focus { border-color: var(--accent); - box-shadow: var(--focus-ring); - background: var(--bg-hover); + box-shadow: 0 0 0 3px var(--focus); + background: rgba(0, 0, 0, 0.2); } :root[data-theme="light"] .config-search__input { - background: white; + background: rgba(255, 255, 255, 0.8); } :root[data-theme="light"] .config-search__input:focus { - background: white; + background: #fff; } .config-search__clear { position: absolute; - right: 22px; + right: 20px; top: 50%; transform: translateY(-50%); - width: 22px; - height: 22px; + width: 20px; + height: 20px; border: none; - border-radius: var(--radius-full); - background: var(--bg-hover); + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); color: var(--muted); - font-size: 14px; + font-size: 16px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease; + background 150ms ease, + color 150ms ease; } .config-search__clear:hover { - background: var(--border-strong); + background: rgba(255, 255, 255, 0.2); color: var(--text); } @@ -133,7 +130,7 @@ .config-nav { flex: 1; overflow-y: auto; - padding: 10px; + padding: 8px; } .config-nav__item { @@ -141,9 +138,9 @@ align-items: center; gap: 12px; width: 100%; - padding: 11px 14px; + padding: 10px 12px; border: none; - border-radius: var(--radius-md); + border-radius: 8px; background: transparent; color: var(--muted); font-size: 13px; @@ -151,21 +148,21 @@ text-align: left; cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease; + background 150ms ease, + color 150ms ease; } .config-nav__item:hover { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.05); color: var(--text); } :root[data-theme="light"] .config-nav__item:hover { - background: rgba(0, 0, 0, 0.04); + background: rgba(0, 0, 0, 0.05); } .config-nav__item.active { - background: var(--accent-subtle); + background: rgba(245, 159, 74, 0.12); color: var(--accent); } @@ -175,13 +172,7 @@ display: flex; align-items: center; justify-content: center; - font-size: 15px; - opacity: 0.7; -} - -.config-nav__item:hover .config-nav__icon, -.config-nav__item.active .config-nav__icon { - opacity: 1; + font-size: 14px; } .config-nav__icon svg { @@ -201,30 +192,30 @@ /* Mode Toggle */ .config-mode-toggle { display: flex; - padding: 4px; - background: var(--bg-elevated); - border-radius: var(--radius-md); + padding: 3px; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; border: 1px solid var(--border); } :root[data-theme="light"] .config-mode-toggle { - background: white; + background: rgba(0, 0, 0, 0.06); } .config-mode-toggle__btn { flex: 1; - padding: 9px 14px; + padding: 8px 12px; border: none; - border-radius: var(--radius-sm); + border-radius: 6px; background: transparent; color: var(--muted); font-size: 12px; font-weight: 600; cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; + background 150ms ease, + color 150ms ease, + box-shadow 150ms ease; } .config-mode-toggle__btn:hover { @@ -232,22 +223,24 @@ } .config-mode-toggle__btn.active { - background: var(--accent); - color: white; - box-shadow: var(--shadow-sm); + background: rgba(255, 255, 255, 0.1); + color: var(--text); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +:root[data-theme="light"] .config-mode-toggle__btn.active { + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } /* =========================================== Main Content =========================================== */ - .config-main { display: flex; flex-direction: column; - min-height: 0; min-width: 0; background: var(--panel); - overflow: hidden; } /* Actions Bar */ @@ -255,28 +248,28 @@ display: flex; align-items: center; justify-content: space-between; - gap: 14px; - padding: 14px 22px; - background: var(--bg-accent); + gap: 12px; + padding: 12px 20px; + background: rgba(0, 0, 0, 0.08); border-bottom: 1px solid var(--border); } :root[data-theme="light"] .config-actions { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.02); } .config-actions__left, .config-actions__right { display: flex; align-items: center; - gap: 10px; + gap: 8px; } .config-changes-badge { - padding: 6px 14px; - border-radius: var(--radius-full); - background: var(--accent-subtle); - border: 1px solid rgba(255, 77, 77, 0.3); + padding: 5px 12px; + border-radius: 999px; + background: rgba(245, 159, 74, 0.15); + border: 1px solid rgba(245, 159, 74, 0.3); color: var(--accent); font-size: 12px; font-weight: 600; @@ -289,10 +282,10 @@ /* Diff Panel */ .config-diff { - margin: 18px 22px 0; - border: 1px solid rgba(255, 77, 77, 0.25); - border-radius: var(--radius-lg); - background: var(--accent-subtle); + margin: 16px 20px 0; + border: 1px solid rgba(245, 159, 74, 0.3); + border-radius: 10px; + background: rgba(245, 159, 74, 0.05); overflow: hidden; } @@ -300,7 +293,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: 14px 18px; + padding: 12px 16px; cursor: pointer; font-size: 13px; font-weight: 600; @@ -315,7 +308,7 @@ .config-diff__chevron { width: 16px; height: 16px; - transition: transform var(--duration-normal) var(--ease-out); + transition: transform 200ms ease; } .config-diff__chevron svg { @@ -328,24 +321,24 @@ } .config-diff__content { - padding: 0 18px 18px; + padding: 0 16px 16px; display: grid; - gap: 10px; + gap: 8px; } .config-diff__item { display: flex; align-items: baseline; - gap: 14px; - padding: 10px 14px; - border-radius: var(--radius-md); - background: var(--bg-elevated); + gap: 12px; + padding: 8px 12px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.1); font-size: 12px; font-family: var(--mono); } :root[data-theme="light"] .config-diff__item { - background: white; + background: rgba(255, 255, 255, 0.6); } .config-diff__path { @@ -357,14 +350,14 @@ .config-diff__values { display: flex; align-items: baseline; - gap: 10px; + gap: 8px; min-width: 0; flex-wrap: wrap; } .config-diff__from { color: var(--danger); - opacity: 0.85; + opacity: 0.8; } .config-diff__arrow { @@ -379,19 +372,19 @@ .config-section-hero { display: flex; align-items: center; - gap: 16px; - padding: 16px 22px; + gap: 14px; + padding: 14px 20px; border-bottom: 1px solid var(--border); - background: var(--bg-accent); + background: rgba(0, 0, 0, 0.04); } :root[data-theme="light"] .config-section-hero { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.015); } .config-section-hero__icon { - width: 30px; - height: 30px; + width: 28px; + height: 28px; color: var(--accent); display: flex; align-items: center; @@ -407,21 +400,17 @@ .config-section-hero__text { display: grid; - gap: 3px; + gap: 2px; min-width: 0; } .config-section-hero__title { - font-size: 16px; + font-size: 15px; font-weight: 600; - letter-spacing: -0.01em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .config-section-hero__desc { - font-size: 13px; + font-size: 12px; color: var(--muted); } @@ -429,59 +418,63 @@ .config-subnav { display: flex; gap: 8px; - padding: 12px 22px 14px; + padding: 10px 20px 12px; border-bottom: 1px solid var(--border); - background: var(--bg-accent); + background: rgba(0, 0, 0, 0.03); overflow-x: auto; } :root[data-theme="light"] .config-subnav { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.02); } .config-subnav__item { border: 1px solid transparent; - border-radius: var(--radius-full); - padding: 7px 14px; + border-radius: 999px; + padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--muted); - background: var(--bg-elevated); + background: rgba(0, 0, 0, 0.12); cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease, - border-color var(--duration-fast) ease; + background 150ms ease, + color 150ms ease, + border-color 150ms ease; white-space: nowrap; } :root[data-theme="light"] .config-subnav__item { - background: white; + background: rgba(0, 0, 0, 0.06); } .config-subnav__item:hover { color: var(--text); - border-color: var(--border); + background: rgba(255, 255, 255, 0.08); +} + +:root[data-theme="light"] .config-subnav__item:hover { + background: rgba(0, 0, 0, 0.08); } .config-subnav__item.active { color: var(--accent); - border-color: rgba(255, 77, 77, 0.4); - background: var(--accent-subtle); + border-color: rgba(245, 159, 74, 0.4); + background: rgba(245, 159, 74, 0.12); } /* Content Area */ .config-content { flex: 1; overflow-y: auto; - padding: 22px; + padding: 20px; } .config-raw-field textarea { min-height: 500px; font-family: var(--mono); font-size: 13px; - line-height: 1.55; + line-height: 1.5; } /* Loading State */ @@ -490,18 +483,18 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 18px; - padding: 80px 24px; + gap: 16px; + padding: 80px 20px; color: var(--muted); } .config-loading__spinner { - width: 40px; - height: 40px; + width: 36px; + height: 36px; border: 3px solid var(--border); border-top-color: var(--accent); - border-radius: var(--radius-full); - animation: spin 0.75s linear infinite; + border-radius: 50%; + animation: spin 0.8s linear infinite; } @keyframes spin { @@ -516,14 +509,14 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 18px; - padding: 80px 24px; + gap: 16px; + padding: 80px 20px; text-align: center; } .config-empty__icon { font-size: 56px; - opacity: 0.35; + opacity: 0.4; } .config-empty__text { @@ -534,44 +527,38 @@ /* =========================================== Section Cards =========================================== */ - .config-form--modern { display: grid; - gap: 26px; + gap: 24px; } .config-section-card { border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--bg-elevated); + border-radius: 12px; + background: rgba(255, 255, 255, 0.02); overflow: hidden; - transition: border-color var(--duration-fast) ease; -} - -.config-section-card:hover { - border-color: var(--border-strong); } :root[data-theme="light"] .config-section-card { - background: white; + background: rgba(255, 255, 255, 0.5); } .config-section-card__header { display: flex; align-items: flex-start; - gap: 16px; - padding: 20px 22px; - background: var(--bg-accent); + gap: 14px; + padding: 18px 20px; + background: rgba(0, 0, 0, 0.06); border-bottom: 1px solid var(--border); } :root[data-theme="light"] .config-section-card__header { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.02); } .config-section-card__icon { - width: 34px; - height: 34px; + width: 32px; + height: 32px; color: var(--accent); flex-shrink: 0; } @@ -590,42 +577,37 @@ margin: 0; font-size: 17px; font-weight: 600; - letter-spacing: -0.01em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .config-section-card__desc { - margin: 5px 0 0; + margin: 4px 0 0; font-size: 13px; color: var(--muted); - line-height: 1.45; + line-height: 1.4; } .config-section-card__content { - padding: 22px; + padding: 20px; } /* =========================================== Form Fields =========================================== */ - .cfg-fields { display: grid; - gap: 22px; + gap: 20px; } .cfg-field { display: grid; - gap: 8px; + gap: 6px; } .cfg-field--error { - padding: 14px; - border-radius: var(--radius-md); - background: var(--danger-subtle); - border: 1px solid rgba(239, 68, 68, 0.3); + padding: 12px; + border-radius: 8px; + background: rgba(255, 92, 92, 0.1); + border: 1px solid rgba(255, 92, 92, 0.3); } .cfg-field__label { @@ -637,7 +619,7 @@ .cfg-field__help { font-size: 12px; color: var(--muted); - line-height: 1.45; + line-height: 1.4; } .cfg-field__error { @@ -648,21 +630,21 @@ /* Text Input */ .cfg-input-wrap { display: flex; - gap: 10px; + gap: 8px; } .cfg-input { flex: 1; - padding: 11px 14px; - border: 1px solid var(--border-strong); - border-radius: var(--radius-md); - background: var(--bg-accent); + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(0, 0, 0, 0.12); font-size: 14px; outline: none; transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - background var(--duration-fast) ease; + border-color 150ms ease, + box-shadow 150ms ease, + background 150ms ease; } .cfg-input::placeholder { @@ -672,38 +654,38 @@ .cfg-input:focus { border-color: var(--accent); - box-shadow: var(--focus-ring); - background: var(--bg-hover); + box-shadow: 0 0 0 3px var(--focus); + background: rgba(0, 0, 0, 0.18); } :root[data-theme="light"] .cfg-input { - background: white; + background: #fff; } :root[data-theme="light"] .cfg-input:focus { - background: white; + background: #fff; } .cfg-input--sm { - padding: 9px 12px; + padding: 8px 10px; font-size: 13px; } .cfg-input__reset { - padding: 10px 14px; + padding: 8px 12px; border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); color: var(--muted); font-size: 14px; cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease; + background 150ms ease, + color 150ms ease; } .cfg-input__reset:hover:not(:disabled) { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.1); color: var(--text); } @@ -715,60 +697,60 @@ /* Textarea */ .cfg-textarea { width: 100%; - padding: 12px 14px; - border: 1px solid var(--border-strong); - border-radius: var(--radius-md); - background: var(--bg-accent); + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(0, 0, 0, 0.12); font-family: var(--mono); font-size: 13px; - line-height: 1.55; + line-height: 1.5; resize: vertical; outline: none; transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; + border-color 150ms ease, + box-shadow 150ms ease; } .cfg-textarea:focus { border-color: var(--accent); - box-shadow: var(--focus-ring); + box-shadow: 0 0 0 3px var(--focus); } :root[data-theme="light"] .cfg-textarea { - background: white; + background: #fff; } .cfg-textarea--sm { - padding: 10px 12px; + padding: 8px 10px; font-size: 12px; } /* Number Input */ .cfg-number { display: inline-flex; - border: 1px solid var(--border-strong); - border-radius: var(--radius-md); + border: 1px solid var(--border); + border-radius: 8px; overflow: hidden; - background: var(--bg-accent); + background: rgba(0, 0, 0, 0.12); } :root[data-theme="light"] .cfg-number { - background: white; + background: #fff; } .cfg-number__btn { - width: 44px; + width: 40px; border: none; - background: var(--bg-elevated); + background: rgba(255, 255, 255, 0.05); color: var(--text); font-size: 18px; font-weight: 300; cursor: pointer; - transition: background var(--duration-fast) ease; + transition: background 150ms ease; } .cfg-number__btn:hover:not(:disabled) { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.1); } .cfg-number__btn:disabled { @@ -777,16 +759,16 @@ } :root[data-theme="light"] .cfg-number__btn { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.03); } :root[data-theme="light"] .cfg-number__btn:hover:not(:disabled) { - background: var(--border); + background: rgba(0, 0, 0, 0.06); } .cfg-number__input { - width: 85px; - padding: 11px; + width: 80px; + padding: 10px; border: none; border-left: 1px solid var(--border); border-right: 1px solid var(--border); @@ -805,57 +787,57 @@ /* Select */ .cfg-select { - padding: 11px 40px 11px 14px; - border: 1px solid var(--border-strong); - border-radius: var(--radius-md); - background-color: var(--bg-accent); + padding: 10px 36px 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background-color: rgba(0, 0, 0, 0.12); 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='%23888' 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 12px center; + background-position: right 10px center; font-size: 14px; cursor: pointer; outline: none; appearance: none; transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; + border-color 150ms ease, + box-shadow 150ms ease; } .cfg-select:focus { border-color: var(--accent); - box-shadow: var(--focus-ring); + box-shadow: 0 0 0 3px var(--focus); } :root[data-theme="light"] .cfg-select { - background-color: white; + background-color: #fff; } /* Segmented Control */ .cfg-segmented { display: inline-flex; - padding: 4px; + padding: 3px; border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-accent); + border-radius: 8px; + background: rgba(0, 0, 0, 0.12); } :root[data-theme="light"] .cfg-segmented { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.04); } .cfg-segmented__btn { - padding: 9px 18px; + padding: 8px 16px; border: none; - border-radius: var(--radius-sm); + border-radius: 6px; background: transparent; color: var(--muted); font-size: 13px; font-weight: 500; cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; + background 150ms ease, + color 150ms ease, + box-shadow 150ms ease; } .cfg-segmented__btn:hover:not(:disabled):not(.active) { @@ -863,9 +845,14 @@ } .cfg-segmented__btn.active { - background: var(--accent); - color: white; - box-shadow: var(--shadow-sm); + background: rgba(255, 255, 255, 0.12); + color: var(--text); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +:root[data-theme="light"] .cfg-segmented__btn.active { + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .cfg-segmented__btn:disabled { @@ -878,33 +865,33 @@ display: flex; align-items: center; justify-content: space-between; - gap: 18px; - padding: 16px 18px; + gap: 16px; + padding: 14px 16px; border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--bg-accent); + border-radius: 10px; + background: rgba(0, 0, 0, 0.06); cursor: pointer; transition: - background var(--duration-fast) ease, - border-color var(--duration-fast) ease; + background 150ms ease, + border-color 150ms ease; } .cfg-toggle-row:hover:not(.disabled) { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.1); border-color: var(--border-strong); } .cfg-toggle-row.disabled { - opacity: 0.55; + opacity: 0.6; cursor: not-allowed; } :root[data-theme="light"] .cfg-toggle-row { - background: white; + background: rgba(255, 255, 255, 0.5); } :root[data-theme="light"] .cfg-toggle-row:hover:not(.disabled) { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.8); } .cfg-toggle-row__content { @@ -921,10 +908,10 @@ .cfg-toggle-row__help { display: block; - margin-top: 3px; + margin-top: 2px; font-size: 12px; color: var(--muted); - line-height: 1.45; + line-height: 1.4; } /* Toggle Switch */ @@ -942,19 +929,19 @@ .cfg-toggle__track { display: block; - width: 50px; + width: 48px; height: 28px; - background: var(--bg-elevated); - border: 1px solid var(--border-strong); - border-radius: var(--radius-full); + background: rgba(255, 255, 255, 0.12); + border: 1px solid var(--border); + border-radius: 999px; position: relative; transition: - background var(--duration-normal) ease, - border-color var(--duration-normal) ease; + background 200ms ease, + border-color 200ms ease; } :root[data-theme="light"] .cfg-toggle__track { - background: var(--border); + background: rgba(0, 0, 0, 0.1); } .cfg-toggle__track::after { @@ -965,51 +952,55 @@ width: 20px; height: 20px; background: var(--text); - border-radius: var(--radius-full); - box-shadow: var(--shadow-sm); + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: - transform var(--duration-normal) var(--ease-out), - background var(--duration-normal) ease; + transform 200ms ease, + background 200ms ease; } .cfg-toggle input:checked + .cfg-toggle__track { - background: var(--ok-subtle); - border-color: rgba(34, 197, 94, 0.4); + background: rgba(43, 217, 127, 0.25); + border-color: rgba(43, 217, 127, 0.5); } .cfg-toggle input:checked + .cfg-toggle__track::after { - transform: translateX(22px); + transform: translateX(20px); background: var(--ok); } .cfg-toggle input:focus + .cfg-toggle__track { - box-shadow: var(--focus-ring); + box-shadow: 0 0 0 3px var(--focus); } /* Object (collapsible) */ .cfg-object { border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--bg-accent); + border-radius: 10px; + background: rgba(0, 0, 0, 0.04); overflow: hidden; } :root[data-theme="light"] .cfg-object { - background: white; + background: rgba(255, 255, 255, 0.4); } .cfg-object__header { display: flex; align-items: center; justify-content: space-between; - padding: 14px 18px; + padding: 12px 16px; cursor: pointer; list-style: none; - transition: background var(--duration-fast) ease; + transition: background 150ms ease; } .cfg-object__header:hover { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.03); +} + +:root[data-theme="light"] .cfg-object__header:hover { + background: rgba(0, 0, 0, 0.02); } .cfg-object__header::-webkit-details-marker { @@ -1026,7 +1017,7 @@ width: 18px; height: 18px; color: var(--muted); - transition: transform var(--duration-normal) var(--ease-out); + transition: transform 200ms ease; } .cfg-object__chevron svg { @@ -1039,36 +1030,36 @@ } .cfg-object__help { - padding: 0 18px 14px; + padding: 0 16px 12px; font-size: 12px; color: var(--muted); border-bottom: 1px solid var(--border); } .cfg-object__content { - padding: 18px; + padding: 16px; display: grid; - gap: 18px; + gap: 16px; } /* Array */ .cfg-array { border: 1px solid var(--border); - border-radius: var(--radius-lg); + border-radius: 10px; overflow: hidden; } .cfg-array__header { display: flex; align-items: center; - gap: 14px; - padding: 14px 18px; - background: var(--bg-accent); + gap: 12px; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.06); border-bottom: 1px solid var(--border); } :root[data-theme="light"] .cfg-array__header { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.02); } .cfg-array__label { @@ -1081,32 +1072,32 @@ .cfg-array__count { font-size: 12px; color: var(--muted); - padding: 4px 10px; - background: var(--bg-elevated); - border-radius: var(--radius-full); + padding: 3px 8px; + background: rgba(255, 255, 255, 0.06); + border-radius: 999px; } :root[data-theme="light"] .cfg-array__count { - background: white; + background: rgba(0, 0, 0, 0.06); } .cfg-array__add { display: inline-flex; align-items: center; gap: 6px; - padding: 7px 14px; + padding: 6px 12px; border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); color: var(--text); font-size: 12px; font-weight: 500; cursor: pointer; - transition: background var(--duration-fast) ease; + transition: background 150ms ease; } .cfg-array__add:hover:not(:disabled) { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.1); } .cfg-array__add:disabled { @@ -1125,14 +1116,14 @@ } .cfg-array__help { - padding: 12px 18px; + padding: 10px 16px; font-size: 12px; color: var(--muted); border-bottom: 1px solid var(--border); } .cfg-array__empty { - padding: 36px 18px; + padding: 32px 16px; text-align: center; color: var(--muted); font-size: 13px; @@ -1152,13 +1143,13 @@ display: flex; align-items: center; justify-content: space-between; - padding: 12px 18px; - background: var(--bg-accent); + padding: 10px 16px; + background: rgba(0, 0, 0, 0.04); border-bottom: 1px solid var(--border); } :root[data-theme="light"] .cfg-array__item-header { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.02); } .cfg-array__item-index { @@ -1166,23 +1157,23 @@ font-weight: 600; color: var(--muted); text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.5px; } .cfg-array__item-remove { - width: 30px; - height: 30px; + width: 28px; + height: 28px; display: flex; align-items: center; justify-content: center; border: none; - border-radius: var(--radius-md); + border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease; + background 150ms ease, + color 150ms ease; } .cfg-array__item-remove svg { @@ -1191,7 +1182,7 @@ } .cfg-array__item-remove:hover:not(:disabled) { - background: var(--danger-subtle); + background: rgba(255, 92, 92, 0.15); color: var(--danger); } @@ -1201,13 +1192,13 @@ } .cfg-array__item-content { - padding: 18px; + padding: 16px; } /* Map (custom entries) */ .cfg-map { border: 1px solid var(--border); - border-radius: var(--radius-lg); + border-radius: 10px; overflow: hidden; } @@ -1215,14 +1206,14 @@ display: flex; align-items: center; justify-content: space-between; - gap: 14px; - padding: 14px 18px; - background: var(--bg-accent); + gap: 12px; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.06); border-bottom: 1px solid var(--border); } :root[data-theme="light"] .cfg-map__header { - background: var(--bg-hover); + background: rgba(0, 0, 0, 0.02); } .cfg-map__label { @@ -1235,19 +1226,19 @@ display: inline-flex; align-items: center; gap: 6px; - padding: 7px 14px; + padding: 6px 12px; border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); color: var(--text); font-size: 12px; font-weight: 500; cursor: pointer; - transition: background var(--duration-fast) ease; + transition: background 150ms ease; } .cfg-map__add:hover:not(:disabled) { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.1); } .cfg-map__add-icon { @@ -1261,7 +1252,7 @@ } .cfg-map__empty { - padding: 28px 18px; + padding: 24px 16px; text-align: center; color: var(--muted); font-size: 13px; @@ -1269,14 +1260,14 @@ .cfg-map__items { display: grid; - gap: 10px; - padding: 14px; + gap: 8px; + padding: 12px; } .cfg-map__item { display: grid; - grid-template-columns: 150px 1fr auto; - gap: 10px; + grid-template-columns: 140px 1fr auto; + gap: 8px; align-items: start; } @@ -1289,19 +1280,19 @@ } .cfg-map__item-remove { - width: 34px; - height: 34px; + width: 32px; + height: 32px; display: flex; align-items: center; justify-content: center; border: none; - border-radius: var(--radius-md); + border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; transition: - background var(--duration-fast) ease, - color var(--duration-fast) ease; + background 150ms ease, + color 150ms ease; } .cfg-map__item-remove svg { @@ -1310,30 +1301,29 @@ } .cfg-map__item-remove:hover:not(:disabled) { - background: var(--danger-subtle); + background: rgba(255, 92, 92, 0.15); color: var(--danger); } /* Pill variants */ .pill--sm { - padding: 5px 12px; + padding: 4px 10px; font-size: 11px; } .pill--ok { - border-color: rgba(34, 197, 94, 0.35); + border-color: rgba(43, 217, 127, 0.4); color: var(--ok); } .pill--danger { - border-color: rgba(239, 68, 68, 0.35); + border-color: rgba(255, 92, 92, 0.4); color: var(--danger); } /* =========================================== Mobile Responsiveness =========================================== */ - @media (max-width: 768px) { .config-layout { grid-template-columns: 1fr; @@ -1345,21 +1335,21 @@ } .config-sidebar__header { - padding: 14px 16px; + padding: 12px 16px; } .config-nav { display: flex; flex-wrap: nowrap; - gap: 6px; - padding: 10px 14px; + gap: 4px; + padding: 8px 12px; overflow-x: auto; -webkit-overflow-scrolling: touch; } .config-nav__item { flex: 0 0 auto; - padding: 9px 14px; + padding: 8px 12px; white-space: nowrap; } @@ -1373,7 +1363,7 @@ .config-actions { flex-wrap: wrap; - padding: 14px 16px; + padding: 12px 16px; } .config-actions__left, @@ -1383,32 +1373,32 @@ } .config-section-hero { - padding: 14px 16px; + padding: 12px 16px; } .config-subnav { - padding: 10px 16px 12px; + padding: 8px 16px 10px; } .config-content { - padding: 18px; + padding: 16px; } .config-section-card__header { - padding: 16px 18px; + padding: 14px 16px; } .config-section-card__content { - padding: 18px; + padding: 16px; } .cfg-toggle-row { - padding: 14px 16px; + padding: 12px 14px; } .cfg-map__item { grid-template-columns: 1fr; - gap: 10px; + gap: 8px; } .cfg-map__item-remove { @@ -1418,9 +1408,9 @@ @media (max-width: 480px) { .config-nav__icon { - width: 26px; - height: 26px; - font-size: 17px; + width: 24px; + height: 24px; + font-size: 16px; } .config-nav__label { @@ -1428,12 +1418,12 @@ } .config-section-card__icon { - width: 30px; - height: 30px; + width: 28px; + height: 28px; } .config-section-card__title { - font-size: 16px; + font-size: 15px; } .cfg-segmented { @@ -1442,6 +1432,6 @@ .cfg-segmented__btn { flex: 1 0 auto; - min-width: 70px; + min-width: 60px; } } diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index c2a5c6fe3..910478853 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -1,14 +1,10 @@ -/* =========================================== - Shell Layout - =========================================== */ - .shell { --shell-pad: 16px; --shell-gap: 16px; --shell-nav-width: 220px; --shell-topbar-height: 56px; - --shell-focus-duration: 200ms; - --shell-focus-ease: var(--ease-out); + --shell-focus-duration: 220ms; + --shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1); height: 100vh; display: grid; grid-template-columns: var(--shell-nav-width) minmax(0, 1fr); @@ -17,7 +13,7 @@ "topbar topbar" "nav content"; gap: 0; - animation: dashboard-enter 0.4s var(--ease-out); + animation: dashboard-enter 0.6s ease-out; transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease); overflow: hidden; } @@ -65,10 +61,6 @@ gap: 0; } -/* =========================================== - Topbar - =========================================== */ - .topbar { grid-area: topbar; position: sticky; @@ -81,7 +73,8 @@ padding: 0 20px; height: var(--shell-topbar-height); border-bottom: 1px solid var(--border); - background: var(--bg); + background: var(--panel); + backdrop-filter: blur(18px); } .topbar-left { @@ -91,84 +84,49 @@ } .topbar .nav-collapse-toggle { - width: 36px; - height: 36px; + width: 44px; + height: 44px; margin-bottom: 0; } .topbar .nav-collapse-toggle__icon { - width: 20px; - height: 20px; + font-size: 22px; } -.topbar .nav-collapse-toggle__icon svg { - width: 20px; - height: 20px; -} - -/* Brand */ .brand { - display: flex; - align-items: center; - gap: 10px; -} - -.brand-logo { - width: 28px; - height: 28px; - flex-shrink: 0; -} - -.brand-logo img { - width: 100%; - height: 100%; - object-fit: contain; -} - -.brand-text { display: flex; flex-direction: column; - gap: 1px; + gap: 2px; } .brand-title { + font-family: var(--font-display); font-size: 16px; - font-weight: 700; - letter-spacing: -0.03em; + letter-spacing: 1px; + text-transform: uppercase; + font-weight: 600; line-height: 1.1; - color: var(--text-strong); } .brand-sub { font-size: 10px; - font-weight: 500; color: var(--muted); - letter-spacing: 0.05em; + letter-spacing: 0.8px; text-transform: uppercase; line-height: 1; } -/* Topbar status */ .topbar-status { display: flex; align-items: center; gap: 8px; } +/* Smaller pill and theme toggle in topbar */ .topbar-status .pill { - padding: 6px 10px; + padding: 4px 10px; gap: 6px; - font-size: 12px; - font-weight: 500; - height: 32px; - box-sizing: border-box; -} - -.topbar-status .pill .mono { - display: flex; - align-items: center; - line-height: 1; - margin-top: 0px; + font-size: 11px; } .topbar-status .statusDot { @@ -177,9 +135,9 @@ } .topbar-status .theme-toggle { - --theme-item: 24px; - --theme-gap: 2px; - --theme-pad: 3px; + --theme-item: 22px; + --theme-gap: 4px; + --theme-pad: 4px; } .topbar-status .theme-icon { @@ -187,26 +145,18 @@ height: 12px; } -/* =========================================== - Navigation Sidebar - =========================================== */ - .nav { grid-area: nav; overflow-y: auto; overflow-x: hidden; - padding: 16px 12px; - background: var(--bg); - scrollbar-width: none; /* Firefox */ + padding: 16px; + border-right: 1px solid var(--border); + background: var(--panel); + backdrop-filter: blur(18px); transition: width var(--shell-focus-duration) var(--shell-focus-ease), - padding var(--shell-focus-duration) var(--shell-focus-ease), - opacity var(--shell-focus-duration) var(--shell-focus-ease); - min-height: 0; -} - -.nav::-webkit-scrollbar { - display: none; /* Chrome/Safari */ + padding var(--shell-focus-duration) var(--shell-focus-ease); + min-height: 0; /* Allow grid item to shrink and enable scrolling */ } .shell--chat-focus .nav { @@ -215,9 +165,9 @@ border-width: 0; overflow: hidden; pointer-events: none; - opacity: 0; } +/* Collapsed nav sidebar - completely hidden */ .nav--collapsed { width: 0; min-width: 0; @@ -228,7 +178,7 @@ pointer-events: none; } -/* Nav collapse toggle */ +/* Nav collapse toggle button */ .nav-collapse-toggle { width: 32px; height: 32px; @@ -237,88 +187,73 @@ justify-content: center; background: transparent; border: 1px solid transparent; - border-radius: var(--radius-md); + border-radius: 6px; cursor: pointer; transition: - background var(--duration-fast) ease, - border-color var(--duration-fast) ease; + background 150ms ease, + border-color 150ms ease; margin-bottom: 16px; } .nav-collapse-toggle:hover { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.08); border-color: var(--border); } +:root[data-theme="light"] .nav-collapse-toggle:hover { + background: rgba(0, 0, 0, 0.06); +} + .nav-collapse-toggle__icon { - display: flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; + font-size: 16px; color: var(--muted); - transition: color var(--duration-fast) ease; } -.nav-collapse-toggle__icon svg { - width: 18px; - height: 18px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.nav-collapse-toggle:hover .nav-collapse-toggle__icon { - color: var(--text); -} - -/* Nav groups */ .nav-group { - margin-bottom: 20px; + margin-bottom: 18px; display: grid; - gap: 2px; + gap: 6px; + padding-bottom: 12px; + border-bottom: 1px dashed rgba(255, 255, 255, 0.08); } .nav-group:last-child { margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; } .nav-group__items { display: grid; - gap: 1px; + gap: 4px; } .nav-group--collapsed .nav-group__items { display: none; } -/* Nav label */ .nav-label { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; - padding: 6px 10px; + padding: 4px 0; font-size: 11px; font-weight: 500; - color: var(--muted); + text-transform: uppercase; + letter-spacing: 1.4px; + color: var(--text); + opacity: 0.7; margin-bottom: 4px; background: transparent; border: none; cursor: pointer; text-align: left; - border-radius: var(--radius-sm); - transition: - color var(--duration-fast) ease, - background var(--duration-fast) ease; } .nav-label:hover { - color: var(--text); - background: var(--bg-hover); + opacity: 1; } .nav-label--static { @@ -326,8 +261,7 @@ } .nav-label--static:hover { - color: var(--muted); - background: transparent; + opacity: 0.7; } .nav-label__text { @@ -335,113 +269,99 @@ } .nav-label__chevron { - font-size: 10px; - opacity: 0.5; - transition: transform var(--duration-fast) ease; + font-size: 12px; + opacity: 0.6; } -.nav-group--collapsed .nav-label__chevron { - transform: rotate(-90deg); -} - -/* Nav items */ .nav-item { position: relative; display: flex; align-items: center; justify-content: flex-start; - gap: 10px; - padding: 8px 10px; - border-radius: var(--radius-md); + gap: 8px; + padding: 10px 12px 10px 14px; + border-radius: 12px; border: 1px solid transparent; background: transparent; color: var(--muted); cursor: pointer; text-decoration: none; transition: - border-color var(--duration-fast) ease, - background var(--duration-fast) ease, - color var(--duration-fast) ease; + border-color 160ms ease, + background 160ms ease, + color 160ms ease; } .nav-item__icon { - width: 16px; - height: 16px; + font-size: 16px; + width: 18px; + height: 18px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; - opacity: 0.7; - transition: opacity var(--duration-fast) ease; -} - -.nav-item__icon svg { - width: 16px; - height: 16px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; } .nav-item__text { font-size: 13px; - font-weight: 500; white-space: nowrap; } .nav-item:hover { color: var(--text); - background: var(--bg-hover); - text-decoration: none; + border-color: rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); } -.nav-item:hover .nav-item__icon { - opacity: 1; +.nav-item::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 4px; + height: 60%; + border-radius: 0 999px 999px 0; + transform: translateY(-50%); + background: transparent; } .nav-item.active { - color: var(--text-strong); - background: var(--accent-subtle); + color: var(--text); + border-color: rgba(245, 159, 74, 0.45); + background: rgba(245, 159, 74, 0.12); } -.nav-item.active .nav-item__icon { - opacity: 1; - color: var(--accent); +.nav-item.active::before { + background: var(--accent); + box-shadow: 0 0 12px rgba(245, 159, 74, 0.4); } -/* =========================================== - Content Area - =========================================== */ - .content { grid-area: content; - padding: 12px 16px 32px; + padding: 8px 6px 20px; display: flex; flex-direction: column; - gap: 24px; + gap: 20px; min-height: 0; - overflow-y: auto; + overflow-y: auto; /* Enable vertical scrolling for pages with long content */ overflow-x: hidden; } -:root[data-theme="light"] .content { - background: var(--bg-content); -} - +/* Chat handles its own scrolling (chat-thread); avoid double scrollbars. */ .content--chat { overflow: hidden; - padding-bottom: 0; } -/* Content header */ +.shell--chat .content { + /* No-op: keep chat layout consistent with other tabs */ +} + .content-header { display: flex; align-items: flex-end; justify-content: space-between; - gap: 16px; - padding: 4px 8px; + gap: 12px; + padding: 0 6px; overflow: hidden; transform-origin: top center; transition: @@ -449,39 +369,35 @@ 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: 80px; + max-height: 90px; } .shell--chat-focus .content-header { opacity: 0; - transform: translateY(-8px); + transform: translateY(-10px); max-height: 0px; padding: 0; pointer-events: none; } .page-title { + font-family: var(--font-display); font-size: 26px; - font-weight: 700; - letter-spacing: -0.035em; - line-height: 1.15; - color: var(--text-strong); + letter-spacing: 0.6px; } .page-sub { color: var(--muted); - font-size: 14px; - font-weight: 400; - margin-top: 6px; - letter-spacing: -0.01em; + font-size: 12px; + letter-spacing: 0.4px; } .page-meta { display: flex; - gap: 8px; + gap: 10px; } -/* Chat view header adjustments */ +/* Chat view: header and controls side by side */ .content--chat .content-header { flex-direction: row; align-items: center; @@ -501,13 +417,9 @@ flex-shrink: 0; } -/* =========================================== - Grid Utilities - =========================================== */ - .grid { display: grid; - gap: 20px; + gap: 18px; } .grid-cols-2 { @@ -521,13 +433,13 @@ .stat-grid { display: grid; gap: 14px; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } .note-grid { display: grid; - gap: 16px; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .row { @@ -538,25 +450,21 @@ .stack { display: grid; - gap: 12px; - grid-template-columns: minmax(0, 1fr); + gap: 14px; } .filters { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 10px; align-items: center; } -/* =========================================== - Responsive - Tablet - =========================================== */ - @media (max-width: 1100px) { .shell { --shell-pad: 12px; --shell-gap: 12px; + --shell-nav-col: 1fr; grid-template-columns: 1fr; grid-template-rows: auto auto 1fr; grid-template-areas: @@ -569,18 +477,17 @@ position: static; max-height: none; display: flex; - gap: 6px; + gap: 16px; overflow-x: auto; border-right: none; - border-bottom: 1px solid var(--border); - padding: 10px 14px; - background: var(--bg); + padding: 12px; } .nav-group { grid-auto-flow: column; - grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); - margin-bottom: 0; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + border-bottom: none; + padding-bottom: 0; } .grid-cols-2, @@ -590,11 +497,13 @@ .topbar { position: static; - padding: 12px 14px; - gap: 10px; + flex-direction: column; + align-items: flex-start; + gap: 12px; } .topbar-status { + width: 100%; flex-wrap: wrap; } diff --git a/ui/src/ui/app-channels.ts b/ui/src/ui/app-channels.ts index 3a3921a8d..09d04597b 100644 --- a/ui/src/ui/app-channels.ts +++ b/ui/src/ui/app-channels.ts @@ -122,9 +122,12 @@ export async function handleNostrProfileSave(host: ClawdbotApp) { }, body: JSON.stringify(state.values), }); - const data = (await response.json().catch(() => null)) as - | { ok?: boolean; error?: string; details?: unknown; persisted?: boolean } - | null; + const data = (await response.json().catch(() => null)) as { + ok?: boolean; + error?: string; + details?: unknown; + persisted?: boolean; + } | null; if (!response.ok || data?.ok === false || !data) { const errorMessage = data?.error ?? `Profile update failed (${response.status})`; @@ -187,9 +190,13 @@ export async function handleNostrProfileImport(host: ClawdbotApp) { }, body: JSON.stringify({ autoMerge: true }), }); - const data = (await response.json().catch(() => null)) as - | { ok?: boolean; error?: string; imported?: NostrProfile; merged?: NostrProfile; saved?: boolean } - | null; + const data = (await response.json().catch(() => null)) as { + ok?: boolean; + error?: string; + imported?: NostrProfile; + merged?: NostrProfile; + saved?: boolean; + } | null; if (!response.ok || data?.ok === false || !data) { const errorMessage = data?.error ?? `Profile import failed (${response.status})`; diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index c5f883716..e7627f05e 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -82,7 +82,10 @@ async function sendChatMessageNow( host.chatAttachments = opts.previousAttachments; } if (ok) { - setLastActiveSessionKey(host as unknown as Parameters[0], host.sessionKey); + setLastActiveSessionKey( + host as unknown as Parameters[0], + host.sessionKey, + ); } if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) { host.chatMessage = opts.previousDraft; @@ -170,7 +173,9 @@ type SessionDefaultsSnapshot = { function resolveAgentIdForSession(host: ChatHost): string | null { const parsed = parseAgentSessionKey(host.sessionKey); if (parsed?.agentId) return parsed.agentId; - const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined; + const snapshot = host.hello?.snapshot as + | { sessionDefaults?: SessionDefaultsSnapshot } + | undefined; const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim(); return fallback || "main"; } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index d9a267a98..4cb9b8530 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -10,12 +10,7 @@ import type { Tab } from "./navigation"; import type { UiSettings } from "./storage"; import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream"; import { flushChatQueueForEvent } from "./app-chat"; -import { - applySettings, - loadCron, - refreshActiveTab, - setLastActiveSessionKey, -} from "./app-settings"; +import { applySettings, loadCron, refreshActiveTab, setLastActiveSessionKey } from "./app-settings"; import { handleChatEvent, type ChatEventPayload } from "./controllers/chat"; import { addExecApproval, @@ -75,8 +70,7 @@ function normalizeSessionKeyForDefaults( raw === "main" || raw === mainKey || (defaultAgentId && - (raw === `agent:${defaultAgentId}:main` || - raw === `agent:${defaultAgentId}:${mainKey}`)); + (raw === `agent:${defaultAgentId}:main` || raw === `agent:${defaultAgentId}:${mainKey}`)); return isAlias ? mainSessionKey : raw; } @@ -185,9 +179,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { const state = handleChatEvent(host as unknown as ClawdbotApp, payload); if (state === "final" || state === "error" || state === "aborted") { resetToolStream(host as unknown as Parameters[0]); - void flushChatQueueForEvent( - host as unknown as Parameters[0], - ); + void flushChatQueueForEvent(host as unknown as Parameters[0]); } if (state === "final") void loadChatHistory(host as unknown as ClawdbotApp); return; diff --git a/ui/src/ui/app-lifecycle.ts b/ui/src/ui/app-lifecycle.ts index 71af9d202..063e984a3 100644 --- a/ui/src/ui/app-lifecycle.ts +++ b/ui/src/ui/app-lifecycle.ts @@ -35,20 +35,11 @@ type LifecycleHost = { export function handleConnected(host: LifecycleHost) { host.basePath = inferBasePath(); - syncTabWithLocation( - host as unknown as Parameters[0], - true, - ); - syncThemeWithSettings( - host as unknown as Parameters[0], - ); - attachThemeListener( - host as unknown as Parameters[0], - ); + syncTabWithLocation(host as unknown as Parameters[0], true); + syncThemeWithSettings(host as unknown as Parameters[0]); + attachThemeListener(host as unknown as Parameters[0]); window.addEventListener("popstate", host.popStateHandler); - applySettingsFromUrl( - host as unknown as Parameters[0], - ); + applySettingsFromUrl(host as unknown as Parameters[0]); connectGateway(host as unknown as Parameters[0]); startNodesPolling(host as unknown as Parameters[0]); if (host.tab === "logs") { @@ -68,17 +59,12 @@ export function handleDisconnected(host: LifecycleHost) { stopNodesPolling(host as unknown as Parameters[0]); stopLogsPolling(host as unknown as Parameters[0]); stopDebugPolling(host as unknown as Parameters[0]); - detachThemeListener( - host as unknown as Parameters[0], - ); + detachThemeListener(host as unknown as Parameters[0]); host.topbarObserver?.disconnect(); host.topbarObserver = null; } -export function handleUpdated( - host: LifecycleHost, - changed: Map, -) { +export function handleUpdated(host: LifecycleHost, changed: Map) { if ( host.tab === "chat" && (changed.has("chatMessages") || diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 22f8d90db..004dd9ff0 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -45,8 +45,39 @@ export function renderChatControls(state: AppViewState) { const showThinking = state.onboarding ? false : state.settings.chatShowThinking; const focusActive = state.onboarding ? true : state.settings.chatFocusMode; // Refresh icon - const refreshIcon = html``; - const focusIcon = html``; + const refreshIcon = html` + + + + + `; + const focusIcon = html` + + + + + + + + `; return html`
- ${state.lastError - ? html`
${state.lastError}
` - : nothing} + ${state.lastError ? html`
${state.lastError}
` : nothing} ${isChat ? renderChatControls(state) : nothing}
- ${state.tab === "overview" - ? renderOverview({ - connected: state.connected, - hello: state.hello, - settings: state.settings, - password: state.password, - lastError: state.lastError, - presenceCount, - sessionsCount, - cronEnabled: state.cronStatus?.enabled ?? null, - cronNext, - lastChannelsRefresh: state.channelsLastSuccess, - onSettingsChange: (next) => state.applySettings(next), - onPasswordChange: (next) => (state.password = next), - onSessionKeyChange: (next) => { - state.sessionKey = next; - state.chatMessage = ""; - state.resetToolStream(); - state.applySettings({ - ...state.settings, - sessionKey: next, - lastActiveSessionKey: next, - }); - void state.loadAssistantIdentity(); - }, - onConnect: () => state.connect(), - onRefresh: () => state.loadOverview(), - }) - : nothing} + ${ + state.tab === "overview" + ? renderOverview({ + connected: state.connected, + hello: state.hello, + settings: state.settings, + password: state.password, + lastError: state.lastError, + presenceCount, + sessionsCount, + cronEnabled: state.cronStatus?.enabled ?? null, + cronNext, + lastChannelsRefresh: state.channelsLastSuccess, + onSettingsChange: (next) => state.applySettings(next), + onPasswordChange: (next) => (state.password = next), + onSessionKeyChange: (next) => { + state.sessionKey = next; + state.chatMessage = ""; + state.resetToolStream(); + state.applySettings({ + ...state.settings, + sessionKey: next, + lastActiveSessionKey: next, + }); + void state.loadAssistantIdentity(); + }, + onConnect: () => state.connect(), + onRefresh: () => state.loadOverview(), + }) + : nothing + } - ${state.tab === "channels" - ? renderChannels({ - connected: state.connected, - loading: state.channelsLoading, - snapshot: state.channelsSnapshot, - lastError: state.channelsError, - lastSuccessAt: state.channelsLastSuccess, - whatsappMessage: state.whatsappLoginMessage, - whatsappQrDataUrl: state.whatsappLoginQrDataUrl, - whatsappConnected: state.whatsappLoginConnected, - whatsappBusy: state.whatsappBusy, - configSchema: state.configSchema, - configSchemaLoading: state.configSchemaLoading, - configForm: state.configForm, - configUiHints: state.configUiHints, - configSaving: state.configSaving, - configFormDirty: state.configFormDirty, - nostrProfileFormState: state.nostrProfileFormState, - nostrProfileAccountId: state.nostrProfileAccountId, - onRefresh: (probe) => loadChannels(state, probe), - onWhatsAppStart: (force) => state.handleWhatsAppStart(force), - onWhatsAppWait: () => state.handleWhatsAppWait(), - onWhatsAppLogout: () => state.handleWhatsAppLogout(), - onConfigPatch: (path, value) => updateConfigFormValue(state, path, value), - onConfigSave: () => state.handleChannelConfigSave(), - onConfigReload: () => state.handleChannelConfigReload(), - onNostrProfileEdit: (accountId, profile) => - state.handleNostrProfileEdit(accountId, profile), - onNostrProfileCancel: () => state.handleNostrProfileCancel(), - onNostrProfileFieldChange: (field, value) => - state.handleNostrProfileFieldChange(field, value), - onNostrProfileSave: () => state.handleNostrProfileSave(), - onNostrProfileImport: () => state.handleNostrProfileImport(), - onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(), - }) - : nothing} + ${ + state.tab === "channels" + ? renderChannels({ + connected: state.connected, + loading: state.channelsLoading, + snapshot: state.channelsSnapshot, + lastError: state.channelsError, + lastSuccessAt: state.channelsLastSuccess, + whatsappMessage: state.whatsappLoginMessage, + whatsappQrDataUrl: state.whatsappLoginQrDataUrl, + whatsappConnected: state.whatsappLoginConnected, + whatsappBusy: state.whatsappBusy, + configSchema: state.configSchema, + configSchemaLoading: state.configSchemaLoading, + configForm: state.configForm, + configUiHints: state.configUiHints, + configSaving: state.configSaving, + configFormDirty: state.configFormDirty, + nostrProfileFormState: state.nostrProfileFormState, + nostrProfileAccountId: state.nostrProfileAccountId, + onRefresh: (probe) => loadChannels(state, probe), + onWhatsAppStart: (force) => state.handleWhatsAppStart(force), + onWhatsAppWait: () => state.handleWhatsAppWait(), + onWhatsAppLogout: () => state.handleWhatsAppLogout(), + onConfigPatch: (path, value) => updateConfigFormValue(state, path, value), + onConfigSave: () => state.handleChannelConfigSave(), + onConfigReload: () => state.handleChannelConfigReload(), + onNostrProfileEdit: (accountId, profile) => + state.handleNostrProfileEdit(accountId, profile), + onNostrProfileCancel: () => state.handleNostrProfileCancel(), + onNostrProfileFieldChange: (field, value) => + state.handleNostrProfileFieldChange(field, value), + onNostrProfileSave: () => state.handleNostrProfileSave(), + onNostrProfileImport: () => state.handleNostrProfileImport(), + onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(), + }) + : nothing + } - ${state.tab === "instances" - ? renderInstances({ - loading: state.presenceLoading, - entries: state.presenceEntries, - lastError: state.presenceError, - statusMessage: state.presenceStatus, - onRefresh: () => loadPresence(state), - }) - : nothing} + ${ + state.tab === "instances" + ? renderInstances({ + loading: state.presenceLoading, + entries: state.presenceEntries, + lastError: state.presenceError, + statusMessage: state.presenceStatus, + onRefresh: () => loadPresence(state), + }) + : nothing + } - ${state.tab === "sessions" - ? renderSessions({ - loading: state.sessionsLoading, - result: state.sessionsResult, - error: state.sessionsError, - activeMinutes: state.sessionsFilterActive, - limit: state.sessionsFilterLimit, - includeGlobal: state.sessionsIncludeGlobal, - includeUnknown: state.sessionsIncludeUnknown, - basePath: state.basePath, - onFiltersChange: (next) => { - state.sessionsFilterActive = next.activeMinutes; - state.sessionsFilterLimit = next.limit; - state.sessionsIncludeGlobal = next.includeGlobal; - state.sessionsIncludeUnknown = next.includeUnknown; - }, - onRefresh: () => loadSessions(state), - onPatch: (key, patch) => patchSession(state, key, patch), - onDelete: (key) => deleteSession(state, key), - }) - : nothing} + ${ + state.tab === "sessions" + ? renderSessions({ + loading: state.sessionsLoading, + result: state.sessionsResult, + error: state.sessionsError, + activeMinutes: state.sessionsFilterActive, + limit: state.sessionsFilterLimit, + includeGlobal: state.sessionsIncludeGlobal, + includeUnknown: state.sessionsIncludeUnknown, + basePath: state.basePath, + onFiltersChange: (next) => { + state.sessionsFilterActive = next.activeMinutes; + state.sessionsFilterLimit = next.limit; + state.sessionsIncludeGlobal = next.includeGlobal; + state.sessionsIncludeUnknown = next.includeUnknown; + }, + onRefresh: () => loadSessions(state), + onPatch: (key, patch) => patchSession(state, key, patch), + onDelete: (key) => deleteSession(state, key), + }) + : nothing + } - ${state.tab === "cron" - ? renderCron({ - loading: state.cronLoading, - status: state.cronStatus, - jobs: state.cronJobs, - error: state.cronError, - busy: state.cronBusy, - form: state.cronForm, - channels: state.channelsSnapshot?.channelMeta?.length - ? state.channelsSnapshot.channelMeta.map((entry) => entry.id) - : state.channelsSnapshot?.channelOrder ?? [], - channelLabels: state.channelsSnapshot?.channelLabels ?? {}, - channelMeta: state.channelsSnapshot?.channelMeta ?? [], - runsJobId: state.cronRunsJobId, - runs: state.cronRuns, - onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }), - onRefresh: () => state.loadCron(), - onAdd: () => addCronJob(state), - onToggle: (job, enabled) => toggleCronJob(state, job, enabled), - onRun: (job) => runCronJob(state, job), - onRemove: (job) => removeCronJob(state, job), - onLoadRuns: (jobId) => loadCronRuns(state, jobId), - }) - : nothing} + ${ + state.tab === "cron" + ? renderCron({ + loading: state.cronLoading, + status: state.cronStatus, + jobs: state.cronJobs, + error: state.cronError, + busy: state.cronBusy, + form: state.cronForm, + channels: state.channelsSnapshot?.channelMeta?.length + ? state.channelsSnapshot.channelMeta.map((entry) => entry.id) + : (state.channelsSnapshot?.channelOrder ?? []), + channelLabels: state.channelsSnapshot?.channelLabels ?? {}, + channelMeta: state.channelsSnapshot?.channelMeta ?? [], + runsJobId: state.cronRunsJobId, + runs: state.cronRuns, + onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }), + onRefresh: () => state.loadCron(), + onAdd: () => addCronJob(state), + onToggle: (job, enabled) => toggleCronJob(state, job, enabled), + onRun: (job) => runCronJob(state, job), + onRemove: (job) => removeCronJob(state, job), + onLoadRuns: (jobId) => loadCronRuns(state, jobId), + }) + : nothing + } - ${state.tab === "skills" - ? renderSkills({ - loading: state.skillsLoading, - report: state.skillsReport, - error: state.skillsError, - filter: state.skillsFilter, - edits: state.skillEdits, - messages: state.skillMessages, - busyKey: state.skillsBusyKey, - onFilterChange: (next) => (state.skillsFilter = next), - onRefresh: () => loadSkills(state, { clearMessages: true }), - onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled), - onEdit: (key, value) => updateSkillEdit(state, key, value), - onSaveKey: (key) => saveSkillApiKey(state, key), - onInstall: (skillKey, name, installId) => - installSkill(state, skillKey, name, installId), - }) - : nothing} + ${ + state.tab === "skills" + ? renderSkills({ + loading: state.skillsLoading, + report: state.skillsReport, + error: state.skillsError, + filter: state.skillsFilter, + edits: state.skillEdits, + messages: state.skillMessages, + busyKey: state.skillsBusyKey, + onFilterChange: (next) => (state.skillsFilter = next), + onRefresh: () => loadSkills(state, { clearMessages: true }), + onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled), + onEdit: (key, value) => updateSkillEdit(state, key, value), + onSaveKey: (key) => saveSkillApiKey(state, key), + onInstall: (skillKey, name, installId) => + installSkill(state, skillKey, name, installId), + }) + : nothing + } - ${state.tab === "nodes" - ? renderNodes({ - loading: state.nodesLoading, - nodes: state.nodes, - devicesLoading: state.devicesLoading, - devicesError: state.devicesError, - devicesList: state.devicesList, - configForm: state.configForm ?? (state.configSnapshot?.config as Record | null), - configLoading: state.configLoading, - configSaving: state.configSaving, - configDirty: state.configFormDirty, - configFormMode: state.configFormMode, - execApprovalsLoading: state.execApprovalsLoading, - execApprovalsSaving: state.execApprovalsSaving, - execApprovalsDirty: state.execApprovalsDirty, - execApprovalsSnapshot: state.execApprovalsSnapshot, - execApprovalsForm: state.execApprovalsForm, - execApprovalsSelectedAgent: state.execApprovalsSelectedAgent, - execApprovalsTarget: state.execApprovalsTarget, - execApprovalsTargetNodeId: state.execApprovalsTargetNodeId, - onRefresh: () => loadNodes(state), - onDevicesRefresh: () => loadDevices(state), - onDeviceApprove: (requestId) => approveDevicePairing(state, requestId), - onDeviceReject: (requestId) => rejectDevicePairing(state, requestId), - onDeviceRotate: (deviceId, role, scopes) => - rotateDeviceToken(state, { deviceId, role, scopes }), - onDeviceRevoke: (deviceId, role) => - revokeDeviceToken(state, { deviceId, role }), - onLoadConfig: () => loadConfig(state), - onLoadExecApprovals: () => { - const target = - state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId - ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } - : { kind: "gateway" as const }; - return loadExecApprovals(state, target); - }, - onBindDefault: (nodeId) => { - if (nodeId) { - updateConfigFormValue(state, ["tools", "exec", "node"], nodeId); - } else { - removeConfigFormValue(state, ["tools", "exec", "node"]); - } - }, - onBindAgent: (agentIndex, nodeId) => { - const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"]; - if (nodeId) { - updateConfigFormValue(state, basePath, nodeId); - } else { - removeConfigFormValue(state, basePath); - } - }, - onSaveBindings: () => saveConfig(state), - onExecApprovalsTargetChange: (kind, nodeId) => { - state.execApprovalsTarget = kind; - state.execApprovalsTargetNodeId = nodeId; - state.execApprovalsSnapshot = null; - state.execApprovalsForm = null; - state.execApprovalsDirty = false; - state.execApprovalsSelectedAgent = null; - }, - onExecApprovalsSelectAgent: (agentId) => { - state.execApprovalsSelectedAgent = agentId; - }, - onExecApprovalsPatch: (path, value) => - updateExecApprovalsFormValue(state, path, value), - onExecApprovalsRemove: (path) => - removeExecApprovalsFormValue(state, path), - onSaveExecApprovals: () => { - const target = - state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId - ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } - : { kind: "gateway" as const }; - return saveExecApprovals(state, target); - }, - }) - : nothing} + ${ + state.tab === "nodes" + ? renderNodes({ + loading: state.nodesLoading, + nodes: state.nodes, + devicesLoading: state.devicesLoading, + devicesError: state.devicesError, + devicesList: state.devicesList, + configForm: + state.configForm ?? + (state.configSnapshot?.config as Record | null), + configLoading: state.configLoading, + configSaving: state.configSaving, + configDirty: state.configFormDirty, + configFormMode: state.configFormMode, + execApprovalsLoading: state.execApprovalsLoading, + execApprovalsSaving: state.execApprovalsSaving, + execApprovalsDirty: state.execApprovalsDirty, + execApprovalsSnapshot: state.execApprovalsSnapshot, + execApprovalsForm: state.execApprovalsForm, + execApprovalsSelectedAgent: state.execApprovalsSelectedAgent, + execApprovalsTarget: state.execApprovalsTarget, + execApprovalsTargetNodeId: state.execApprovalsTargetNodeId, + onRefresh: () => loadNodes(state), + onDevicesRefresh: () => loadDevices(state), + onDeviceApprove: (requestId) => approveDevicePairing(state, requestId), + onDeviceReject: (requestId) => rejectDevicePairing(state, requestId), + onDeviceRotate: (deviceId, role, scopes) => + rotateDeviceToken(state, { deviceId, role, scopes }), + onDeviceRevoke: (deviceId, role) => revokeDeviceToken(state, { deviceId, role }), + onLoadConfig: () => loadConfig(state), + onLoadExecApprovals: () => { + const target = + state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId + ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } + : { kind: "gateway" as const }; + return loadExecApprovals(state, target); + }, + onBindDefault: (nodeId) => { + if (nodeId) { + updateConfigFormValue(state, ["tools", "exec", "node"], nodeId); + } else { + removeConfigFormValue(state, ["tools", "exec", "node"]); + } + }, + onBindAgent: (agentIndex, nodeId) => { + const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"]; + if (nodeId) { + updateConfigFormValue(state, basePath, nodeId); + } else { + removeConfigFormValue(state, basePath); + } + }, + onSaveBindings: () => saveConfig(state), + onExecApprovalsTargetChange: (kind, nodeId) => { + state.execApprovalsTarget = kind; + state.execApprovalsTargetNodeId = nodeId; + state.execApprovalsSnapshot = null; + state.execApprovalsForm = null; + state.execApprovalsDirty = false; + state.execApprovalsSelectedAgent = null; + }, + onExecApprovalsSelectAgent: (agentId) => { + state.execApprovalsSelectedAgent = agentId; + }, + onExecApprovalsPatch: (path, value) => + updateExecApprovalsFormValue(state, path, value), + onExecApprovalsRemove: (path) => removeExecApprovalsFormValue(state, path), + onSaveExecApprovals: () => { + const target = + state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId + ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } + : { kind: "gateway" as const }; + return saveExecApprovals(state, target); + }, + }) + : nothing + } - ${state.tab === "chat" +${state.tab === "chat" ? renderChat({ sessionKey: state.sessionKey, onSessionKeyChange: (next) => { @@ -499,83 +487,89 @@ export function renderApp(state: AppViewState) { }) : nothing} - ${state.tab === "config" - ? renderConfig({ - raw: state.configRaw, - originalRaw: state.configRawOriginal, - valid: state.configValid, - issues: state.configIssues, - loading: state.configLoading, - saving: state.configSaving, - applying: state.configApplying, - updating: state.updateRunning, - connected: state.connected, - schema: state.configSchema, - schemaLoading: state.configSchemaLoading, - uiHints: state.configUiHints, - formMode: state.configFormMode, - formValue: state.configForm, - originalValue: state.configFormOriginal, - searchQuery: state.configSearchQuery, - activeSection: state.configActiveSection, - activeSubsection: state.configActiveSubsection, - onRawChange: (next) => { - state.configRaw = next; - }, - onFormModeChange: (mode) => (state.configFormMode = mode), - onFormPatch: (path, value) => updateConfigFormValue(state, path, value), - onSearchChange: (query) => (state.configSearchQuery = query), - onSectionChange: (section) => { - state.configActiveSection = section; - state.configActiveSubsection = null; - }, - onSubsectionChange: (section) => (state.configActiveSubsection = section), - onReload: () => loadConfig(state), - onSave: () => saveConfig(state), - onApply: () => applyConfig(state), - onUpdate: () => runUpdate(state), - }) - : nothing} + ${ + state.tab === "config" + ? renderConfig({ + raw: state.configRaw, + originalRaw: state.configRawOriginal, + valid: state.configValid, + issues: state.configIssues, + loading: state.configLoading, + saving: state.configSaving, + applying: state.configApplying, + updating: state.updateRunning, + connected: state.connected, + schema: state.configSchema, + schemaLoading: state.configSchemaLoading, + uiHints: state.configUiHints, + formMode: state.configFormMode, + formValue: state.configForm, + originalValue: state.configFormOriginal, + searchQuery: state.configSearchQuery, + activeSection: state.configActiveSection, + activeSubsection: state.configActiveSubsection, + onRawChange: (next) => { + state.configRaw = next; + }, + onFormModeChange: (mode) => (state.configFormMode = mode), + onFormPatch: (path, value) => updateConfigFormValue(state, path, value), + onSearchChange: (query) => (state.configSearchQuery = query), + onSectionChange: (section) => { + state.configActiveSection = section; + state.configActiveSubsection = null; + }, + onSubsectionChange: (section) => (state.configActiveSubsection = section), + onReload: () => loadConfig(state), + onSave: () => saveConfig(state), + onApply: () => applyConfig(state), + onUpdate: () => runUpdate(state), + }) + : nothing + } - ${state.tab === "debug" - ? renderDebug({ - loading: state.debugLoading, - status: state.debugStatus, - health: state.debugHealth, - models: state.debugModels, - heartbeat: state.debugHeartbeat, - eventLog: state.eventLog, - callMethod: state.debugCallMethod, - callParams: state.debugCallParams, - callResult: state.debugCallResult, - callError: state.debugCallError, - onCallMethodChange: (next) => (state.debugCallMethod = next), - onCallParamsChange: (next) => (state.debugCallParams = next), - onRefresh: () => loadDebug(state), - onCall: () => callDebugMethod(state), - }) - : nothing} + ${ + state.tab === "debug" + ? renderDebug({ + loading: state.debugLoading, + status: state.debugStatus, + health: state.debugHealth, + models: state.debugModels, + heartbeat: state.debugHeartbeat, + eventLog: state.eventLog, + callMethod: state.debugCallMethod, + callParams: state.debugCallParams, + callResult: state.debugCallResult, + callError: state.debugCallError, + onCallMethodChange: (next) => (state.debugCallMethod = next), + onCallParamsChange: (next) => (state.debugCallParams = next), + onRefresh: () => loadDebug(state), + onCall: () => callDebugMethod(state), + }) + : nothing + } - ${state.tab === "logs" - ? renderLogs({ - loading: state.logsLoading, - error: state.logsError, - file: state.logsFile, - entries: state.logsEntries, - filterText: state.logsFilterText, - levelFilters: state.logsLevelFilters, - autoFollow: state.logsAutoFollow, - truncated: state.logsTruncated, - onFilterTextChange: (next) => (state.logsFilterText = next), - onLevelToggle: (level, enabled) => { - state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled }; - }, - onToggleAutoFollow: (next) => (state.logsAutoFollow = next), - onRefresh: () => loadLogs(state, { reset: true }), - onExport: (lines, label) => state.exportLogs(lines, label), - onScroll: (event) => state.handleLogsScroll(event), - }) - : nothing} + ${ + state.tab === "logs" + ? renderLogs({ + loading: state.logsLoading, + error: state.logsError, + file: state.logsFile, + entries: state.logsEntries, + filterText: state.logsFilterText, + levelFilters: state.logsLevelFilters, + autoFollow: state.logsAutoFollow, + truncated: state.logsTruncated, + onFilterTextChange: (next) => (state.logsFilterText = next), + onLevelToggle: (level, enabled) => { + state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled }; + }, + onToggleAutoFollow: (next) => (state.logsAutoFollow = next), + onRefresh: () => loadLogs(state, { reset: true }), + onExport: (lines, label) => state.exportLogs(lines, label), + onScroll: (event) => state.handleLogsScroll(event), + }) + : nothing + } ${renderExecApprovalPrompt(state)} diff --git a/ui/src/ui/app-scroll.ts b/ui/src/ui/app-scroll.ts index 5955e80b8..22fcc0fbd 100644 --- a/ui/src/ui/app-scroll.ts +++ b/ui/src/ui/app-scroll.ts @@ -35,8 +35,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) { host.chatScrollFrame = null; const target = pickScrollTarget(); if (!target) return; - const distanceFromBottom = - target.scrollHeight - target.scrollTop - target.clientHeight; + const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight; const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200; if (!shouldStick) return; if (force) host.chatHasAutoScrolled = true; @@ -49,8 +48,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) { if (!latest) return; const latestDistanceFromBottom = latest.scrollHeight - latest.scrollTop - latest.clientHeight; - const shouldStickRetry = - force || host.chatUserNearBottom || latestDistanceFromBottom < 200; + const shouldStickRetry = force || host.chatUserNearBottom || latestDistanceFromBottom < 200; if (!shouldStickRetry) return; latest.scrollTop = latest.scrollHeight; host.chatUserNearBottom = true; @@ -78,16 +76,14 @@ export function scheduleLogsScroll(host: ScrollHost, force = false) { export function handleChatScroll(host: ScrollHost, event: Event) { const container = event.currentTarget as HTMLElement | null; if (!container) return; - const distanceFromBottom = - container.scrollHeight - container.scrollTop - container.clientHeight; + const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; host.chatUserNearBottom = distanceFromBottom < 200; } export function handleLogsScroll(host: ScrollHost, event: Event) { const container = event.currentTarget as HTMLElement | null; if (!container) return; - const distanceFromBottom = - container.scrollHeight - container.scrollTop - container.clientHeight; + const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; host.logsAtBottom = distanceFromBottom < 80; } diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 0cceff4cb..06019b472 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -9,12 +9,24 @@ import { loadExecApprovals } from "./controllers/exec-approvals"; import { loadPresence } from "./controllers/presence"; import { loadSessions } from "./controllers/sessions"; import { loadSkills } from "./controllers/skills"; -import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation"; +import { + inferBasePathFromPathname, + normalizeBasePath, + normalizePath, + pathForTab, + tabFromPath, + type Tab, +} from "./navigation"; import { saveSettings, type UiSettings } from "./storage"; import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition"; import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; -import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling"; +import { + startLogsPolling, + stopLogsPolling, + startDebugPolling, + stopDebugPolling, +} from "./app-polling"; import { refreshChat } from "./app-chat"; import type { ClawdbotApp } from "./app"; @@ -113,8 +125,7 @@ export function applySettingsFromUrl(host: SettingsHost) { export function setTab(host: SettingsHost, next: Tab) { if (host.tab !== next) host.tab = next; if (next === "chat") host.chatHasAutoScrolled = false; - if (next === "logs") - startLogsPolling(host as unknown as Parameters[0]); + if (next === "logs") startLogsPolling(host as unknown as Parameters[0]); else stopLogsPolling(host as unknown as Parameters[0]); if (next === "debug") startDebugPolling(host as unknown as Parameters[0]); @@ -123,11 +134,7 @@ export function setTab(host: SettingsHost, next: Tab) { syncUrlWithTab(host, next, false); } -export function setTheme( - host: SettingsHost, - next: ThemeMode, - context?: ThemeTransitionContext, -) { +export function setTheme(host: SettingsHost, next: ThemeMode, context?: ThemeTransitionContext) { const applyTheme = () => { host.theme = next; applySettings(host, { ...host.settings, theme: next }); @@ -172,10 +179,7 @@ export async function refreshActiveTab(host: SettingsHost) { if (host.tab === "logs") { host.logsAtBottom = true; await loadLogs(host as unknown as ClawdbotApp, { reset: true }); - scheduleLogsScroll( - host as unknown as Parameters[0], - true, - ); + scheduleLogsScroll(host as unknown as Parameters[0], true); } } @@ -261,8 +265,7 @@ export function onPopState(host: SettingsHost) { export function setTabFromRoute(host: SettingsHost, next: Tab) { if (host.tab !== next) host.tab = next; if (next === "chat") host.chatHasAutoScrolled = false; - if (next === "logs") - startLogsPolling(host as unknown as Parameters[0]); + if (next === "logs") startLogsPolling(host as unknown as Parameters[0]); else stopLogsPolling(host as unknown as Parameters[0]); if (next === "debug") startDebugPolling(host as unknown as Parameters[0]); @@ -293,11 +296,7 @@ export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) { } } -export function syncUrlWithSessionKey( - host: SettingsHost, - sessionKey: string, - replace: boolean, -) { +export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, replace: boolean) { if (typeof window === "undefined") return; const url = new URL(window.location.href); url.searchParams.set("session", sessionKey); diff --git a/ui/src/ui/app-tool-stream.ts b/ui/src/ui/app-tool-stream.ts index 2fbe12b7a..f43814998 100644 --- a/ui/src/ui/app-tool-stream.ts +++ b/ui/src/ui/app-tool-stream.ts @@ -191,8 +191,7 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo } if (payload.stream !== "tool") return; - const sessionKey = - typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; + const sessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; if (sessionKey && sessionKey !== host.sessionKey) return; // Fallback: only accept session-less events for the active run. if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return; diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 069465e32..f7e76052c 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -22,10 +22,7 @@ import type { import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types"; import type { EventLogEntry } from "./app-events"; import type { SkillMessage } from "./controllers/skills"; -import type { - ExecApprovalsFile, - ExecApprovalsSnapshot, -} from "./controllers/exec-approvals"; +import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals"; import type { DevicePairingList } from "./controllers/devices"; import type { ExecApprovalRequest } from "./controllers/exec-approval"; import type { NostrProfileFormState } from "./views/channels.nostr-profile-form"; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 649e76342..34d997aab 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -1,4 +1,4 @@ -import { LitElement, html, nothing } from "lit"; +import { LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway"; @@ -27,10 +27,7 @@ import type { import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types"; import type { EventLogEntry } from "./app-events"; import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults"; -import type { - ExecApprovalsFile, - ExecApprovalsSnapshot, -} from "./controllers/exec-approvals"; +import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals"; import type { DevicePairingList } from "./controllers/devices"; import type { ExecApprovalRequest } from "./controllers/exec-approval"; import { @@ -259,9 +256,7 @@ export class ClawdbotApp extends LitElement { private toolStreamOrder: string[] = []; basePath = ""; private popStateHandler = () => - onPopStateInternal( - this as unknown as Parameters[0], - ); + onPopStateInternal(this as unknown as Parameters[0]); private themeMedia: MediaQueryList | null = null; private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null; private topbarObserver: ResizeObserver | null = null; @@ -285,16 +280,11 @@ export class ClawdbotApp extends LitElement { } protected updated(changed: Map) { - handleUpdated( - this as unknown as Parameters[0], - changed, - ); + handleUpdated(this as unknown as Parameters[0], changed); } connect() { - connectGatewayInternal( - this as unknown as Parameters[0], - ); + connectGatewayInternal(this as unknown as Parameters[0]); } handleChatScroll(event: Event) { @@ -316,15 +306,11 @@ export class ClawdbotApp extends LitElement { } resetToolStream() { - resetToolStreamInternal( - this as unknown as Parameters[0], - ); + resetToolStreamInternal(this as unknown as Parameters[0]); } resetChatScroll() { - resetChatScrollInternal( - this as unknown as Parameters[0], - ); + resetChatScrollInternal(this as unknown as Parameters[0]); } async loadAssistantIdentity() { @@ -332,10 +318,7 @@ export class ClawdbotApp extends LitElement { } applySettings(next: UiSettings) { - applySettingsInternal( - this as unknown as Parameters[0], - next, - ); + applySettingsInternal(this as unknown as Parameters[0], next); } setTab(next: Tab) { @@ -343,29 +326,19 @@ export class ClawdbotApp extends LitElement { } setTheme(next: ThemeMode, context?: Parameters[2]) { - setThemeInternal( - this as unknown as Parameters[0], - next, - context, - ); + setThemeInternal(this as unknown as Parameters[0], next, context); } async loadOverview() { - await loadOverviewInternal( - this as unknown as Parameters[0], - ); + await loadOverviewInternal(this as unknown as Parameters[0]); } async loadCron() { - await loadCronInternal( - this as unknown as Parameters[0], - ); + await loadCronInternal(this as unknown as Parameters[0]); } async handleAbortChat() { - await handleAbortChatInternal( - this as unknown as Parameters[0], - ); + await handleAbortChatInternal(this as unknown as Parameters[0]); } removeQueuedMessage(id: string) { diff --git a/ui/src/ui/assistant-identity.ts b/ui/src/ui/assistant-identity.ts index c21ac6de6..b11b5eeac 100644 --- a/ui/src/ui/assistant-identity.ts +++ b/ui/src/ui/assistant-identity.ts @@ -28,13 +28,10 @@ function coerceIdentityValue(value: string | undefined, maxLength: number): stri export function normalizeAssistantIdentity( input?: Partial | null, ): AssistantIdentity { - const name = - coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME; + const name = coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME; const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null; const agentId = - typeof input?.agentId === "string" && input.agentId.trim() - ? input.agentId.trim() - : null; + typeof input?.agentId === "string" && input.agentId.trim() ? input.agentId.trim() : null; return { agentId, name, avatar }; } diff --git a/ui/src/ui/chat-markdown.browser.test.ts b/ui/src/ui/chat-markdown.browser.test.ts index 732e4b86a..3548dc35c 100644 --- a/ui/src/ui/chat-markdown.browser.test.ts +++ b/ui/src/ui/chat-markdown.browser.test.ts @@ -46,9 +46,7 @@ describe("chat markdown rendering", () => { await app.updateComplete; - const toolCards = Array.from( - app.querySelectorAll(".chat-tool-card"), - ); + const toolCards = Array.from(app.querySelectorAll(".chat-tool-card")); const toolCard = toolCards.find((card) => card.querySelector(".chat-tool-card__preview, .chat-tool-card__inline"), ); diff --git a/ui/src/ui/chat/copy-as-markdown.ts b/ui/src/ui/chat/copy-as-markdown.ts index 1309f08b8..5115b3d9e 100644 --- a/ui/src/ui/chat/copy-as-markdown.ts +++ b/ui/src/ui/chat/copy-as-markdown.ts @@ -1,11 +1,14 @@ import { html, type TemplateResult } from "lit"; -import { icons } from "../icons"; +import { renderEmojiIcon, setEmojiIcon } from "../icons"; const COPIED_FOR_MS = 1500; const ERROR_FOR_MS = 2000; const COPY_LABEL = "Copy as markdown"; const COPIED_LABEL = "Copied"; const ERROR_LABEL = "Copy failed"; +const COPY_ICON = "📋"; +const COPIED_ICON = "✓"; +const ERROR_ICON = "!"; type CopyButtonOptions = { text: () => string; @@ -38,9 +41,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult { aria-label=${idleLabel} @click=${async (e: Event) => { const btn = e.currentTarget as HTMLButtonElement | null; - const iconContainer = btn?.querySelector( - ".chat-copy-btn__icon", - ) as HTMLElement | null; + const icon = btn?.querySelector(".chat-copy-btn__icon") as HTMLElement | null; if (!btn || btn.dataset.copying === "1") return; @@ -58,29 +59,30 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult { if (!copied) { btn.dataset.error = "1"; setButtonLabel(btn, ERROR_LABEL); + setEmojiIcon(icon, ERROR_ICON); window.setTimeout(() => { if (!btn.isConnected) return; delete btn.dataset.error; setButtonLabel(btn, idleLabel); + setEmojiIcon(icon, COPY_ICON); }, ERROR_FOR_MS); return; } btn.dataset.copied = "1"; setButtonLabel(btn, COPIED_LABEL); + setEmojiIcon(icon, COPIED_ICON); window.setTimeout(() => { if (!btn.isConnected) return; delete btn.dataset.copied; setButtonLabel(btn, idleLabel); + setEmojiIcon(icon, COPY_ICON); }, COPIED_FOR_MS); }} > - + ${renderEmojiIcon(COPY_ICON, "chat-copy-btn__icon")} `; } diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 4a9ccec14..b5e024013 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -122,11 +122,7 @@ export function renderMessageGroup( ? assistantName : normalizedRole; const roleClass = - normalizedRole === "user" - ? "user" - : normalizedRole === "assistant" - ? "assistant" - : "other"; + normalizedRole === "user" ? "user" : normalizedRole === "assistant" ? "assistant" : "other"; const timestamp = new Date(group.timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", @@ -143,8 +139,7 @@ export function renderMessageGroup( renderGroupedMessage( item.message, { - isStreaming: - group.isStreaming && index === group.messages.length - 1, + isStreaming: group.isStreaming && index === group.messages.length - 1, showReasoning: opts.showReasoning, }, opts.onOpenSidebar, @@ -159,10 +154,7 @@ export function renderMessageGroup( `; } -function renderAvatar( - role: string, - assistant?: Pick, -) { +function renderAvatar(role: string, assistant?: Pick) { const normalized = normalizeRoleForGrouping(role); const assistantName = assistant?.name?.trim() || "Assistant"; const assistantAvatar = assistant?.avatar?.trim() || ""; @@ -179,7 +171,7 @@ function renderAvatar( ? "user" : normalized === "assistant" ? "assistant" - : normalized === "tool" + : normalized === "tool" ? "tool" : "other"; @@ -199,9 +191,7 @@ function renderAvatar( function isAvatarUrl(value: string): boolean { return ( - /^https?:\/\//i.test(value) || - /^data:image\//i.test(value) || - /^\//.test(value) // Relative paths from avatar endpoint + /^https?:\/\//i.test(value) || /^data:image\//i.test(value) || value.startsWith('/') // Relative paths from avatar endpoint ); } @@ -245,13 +235,9 @@ function renderGroupedMessage( const extractedText = extractTextCached(message); const extractedThinking = - opts.showReasoning && role === "assistant" - ? extractThinkingCached(message) - : null; + opts.showReasoning && role === "assistant" ? extractThinkingCached(message) : null; const markdownBase = extractedText?.trim() ? extractedText : null; - const reasoningMarkdown = extractedThinking - ? formatReasoningMarkdown(extractedThinking) - : null; + const reasoningMarkdown = extractedThinking ? formatReasoningMarkdown(extractedThinking) : null; const markdown = markdownBase; const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim()); @@ -265,9 +251,7 @@ function renderGroupedMessage( .join(" "); if (!markdown && hasToolCards && isToolResult) { - return html`${toolCards.map((card) => - renderToolCardSidebar(card, onOpenSidebar), - )}`; + return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`; } if (!markdown && !hasToolCards && !hasImages) return nothing; diff --git a/ui/src/ui/chat/message-extract.ts b/ui/src/ui/chat/message-extract.ts index 76dcfa591..6a63a073f 100644 --- a/ui/src/ui/chat/message-extract.ts +++ b/ui/src/ui/chat/message-extract.ts @@ -90,13 +90,9 @@ export function extractThinking(message: unknown): string | null { const rawText = extractRawText(message); if (!rawText) return null; const matches = [ - ...rawText.matchAll( - /<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi, - ), + ...rawText.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi), ]; - const extracted = matches - .map((m) => (m[1] ?? "").trim()) - .filter(Boolean); + const extracted = matches.map((m) => (m[1] ?? "").trim()).filter(Boolean); return extracted.length > 0 ? extracted.join("\n") : null; } diff --git a/ui/src/ui/chat/message-normalizer.test.ts b/ui/src/ui/chat/message-normalizer.test.ts index 132a4be17..c9c241b07 100644 --- a/ui/src/ui/chat/message-normalizer.test.ts +++ b/ui/src/ui/chat/message-normalizer.test.ts @@ -44,8 +44,18 @@ describe("message-normalizer", () => { expect(result.role).toBe("assistant"); expect(result.content).toHaveLength(2); - expect(result.content[0]).toEqual({ type: "text", text: "Here is the result", name: undefined, args: undefined }); - expect(result.content[1]).toEqual({ type: "tool_use", text: undefined, name: "bash", args: { command: "ls" } }); + expect(result.content[0]).toEqual({ + type: "text", + text: "Here is the result", + name: undefined, + args: undefined, + }); + expect(result.content[1]).toEqual({ + type: "tool_use", + text: undefined, + name: "bash", + args: { command: "ls" }, + }); }); it("normalizes message with text field (alternative format)", () => { diff --git a/ui/src/ui/chat/message-normalizer.ts b/ui/src/ui/chat/message-normalizer.ts index e4cbab81d..388939b9f 100644 --- a/ui/src/ui/chat/message-normalizer.ts +++ b/ui/src/ui/chat/message-normalizer.ts @@ -2,10 +2,7 @@ * Message normalization utilities for chat rendering. */ -import type { - NormalizedMessage, - MessageContentItem, -} from "../types/chat-types"; +import type { NormalizedMessage, MessageContentItem } from "../types/chat-types"; /** * Normalize a raw message object into a consistent structure. @@ -16,8 +13,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage { // Detect tool messages by common gateway shapes. // Some tool events come through as assistant role with tool_* items in the content array. - const hasToolId = - typeof m.toolCallId === "string" || typeof m.tool_call_id === "string"; + const hasToolId = typeof m.toolCallId === "string" || typeof m.tool_call_id === "string"; const contentRaw = m.content; const contentItems = Array.isArray(contentRaw) ? contentRaw : null; diff --git a/ui/src/ui/chat/tool-cards.ts b/ui/src/ui/chat/tool-cards.ts index bf82fa49a..8db5e5a8d 100644 --- a/ui/src/ui/chat/tool-cards.ts +++ b/ui/src/ui/chat/tool-cards.ts @@ -1,13 +1,9 @@ import { html, nothing } from "lit"; import { formatToolDetail, resolveToolDisplay } from "../tool-display"; -import { icons } from "../icons"; import type { ToolCard } from "../types/chat-types"; import { TOOL_INLINE_THRESHOLD } from "./constants"; -import { - formatToolOutputForSidebar, - getTruncatedPreview, -} from "./tool-helpers"; +import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers"; import { isToolResultMessage } from "./message-normalizer"; import { extractTextCached } from "./message-extract"; @@ -38,10 +34,7 @@ export function extractToolCards(message: unknown): ToolCard[] { cards.push({ kind: "result", name, text }); } - if ( - isToolResultMessage(message) && - !cards.some((card) => card.kind === "result") - ) { + if (isToolResultMessage(message) && !cards.some((card) => card.kind === "result")) { const name = (typeof m.toolName === "string" && m.toolName) || (typeof m.tool_name === "string" && m.tool_name) || @@ -53,10 +46,7 @@ export function extractToolCards(message: unknown): ToolCard[] { return cards; } -export function renderToolCardSidebar( - card: ToolCard, - onOpenSidebar?: (content: string) => void, -) { +export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content: string) => void) { const display = resolveToolDisplay({ name: card.name, args: card.args }); const detail = formatToolDetail(display); const hasText = Boolean(card.text?.trim()); @@ -86,36 +76,48 @@ export function renderToolCardSidebar( @click=${handleClick} role=${canClick ? "button" : nothing} tabindex=${canClick ? "0" : nothing} - @keydown=${canClick - ? (e: KeyboardEvent) => { - if (e.key !== "Enter" && e.key !== " ") return; - e.preventDefault(); - handleClick?.(); - } - : nothing} + @keydown=${ + canClick + ? (e: KeyboardEvent) => { + if (e.key !== "Enter" && e.key !== " ") return; + e.preventDefault(); + handleClick?.(); + } + : nothing + } >
- ${icons[display.icon]} + ${display.emoji} ${display.label}
- ${canClick - ? html`${hasText ? "View" : ""} ${icons.check}` - : nothing} - ${isEmpty && !canClick ? html`${icons.check}` : nothing} + ${ + canClick + ? html`${hasText ? "View ›" : "›"}` + : nothing + } + ${ + isEmpty && !canClick + ? html` + + ` + : nothing + }
- ${detail - ? html`
${detail}
` - : nothing} - ${isEmpty - ? html`
Completed
` - : nothing} - ${showCollapsed - ? html`
${getTruncatedPreview(card.text!)}
` - : nothing} - ${showInline - ? html`
${card.text}
` - : nothing} + ${detail ? html`
${detail}
` : nothing} + ${ + isEmpty + ? html` +
Completed
+ ` + : nothing + } + ${ + showCollapsed + ? html`
${getTruncatedPreview(card.text!)}
` + : nothing + } + ${showInline ? html`
${card.text}
` : nothing} `; } diff --git a/ui/src/ui/chat/tool-helpers.test.ts b/ui/src/ui/chat/tool-helpers.test.ts index 432bd4f7a..d1e166233 100644 --- a/ui/src/ui/chat/tool-helpers.test.ts +++ b/ui/src/ui/chat/tool-helpers.test.ts @@ -16,7 +16,7 @@ describe("tool-helpers", () => { }); it("formats valid JSON array as code block", () => { - const input = '[1, 2, 3]'; + const input = "[1, 2, 3]"; const result = formatToolOutputForSidebar(input); expect(result).toBe(`\`\`\`json diff --git a/ui/src/ui/components/resizable-divider.ts b/ui/src/ui/components/resizable-divider.ts index a14cca35a..98aba4bc6 100644 --- a/ui/src/ui/components/resizable-divider.ts +++ b/ui/src/ui/components/resizable-divider.ts @@ -24,7 +24,7 @@ export class ResizableDivider extends LitElement { flex-shrink: 0; position: relative; } - + :host::before { content: ""; position: absolute; @@ -33,18 +33,20 @@ export class ResizableDivider extends LitElement { right: -4px; bottom: 0; } - + :host(:hover) { background: var(--accent, #007bff); } - + :host(.dragging) { background: var(--accent, #007bff); } `; render() { - return html``; + return html` + + `; } connectedCallback() { @@ -89,7 +91,7 @@ export class ResizableDivider extends LitElement { detail: { splitRatio: newRatio }, bubbles: true, composed: true, - }) + }), ); }; diff --git a/ui/src/ui/config-form.browser.test.ts b/ui/src/ui/config-form.browser.test.ts index 92d1982d6..848acddf4 100644 --- a/ui/src/ui/config-form.browser.test.ts +++ b/ui/src/ui/config-form.browser.test.ts @@ -29,12 +29,7 @@ const rootSchema = { type: "boolean", }, bind: { - anyOf: [ - { const: "auto" }, - { const: "lan" }, - { const: "tailnet" }, - { const: "loopback" }, - ], + anyOf: [{ const: "auto" }, { const: "lan" }, { const: "tailnet" }, { const: "loopback" }], }, }, }; @@ -57,17 +52,12 @@ describe("config form renderer", () => { container, ); - const tokenInput = container.querySelector( - "input[type='password']", - ) as HTMLInputElement | null; + const tokenInput = container.querySelector("input[type='password']") as HTMLInputElement | null; expect(tokenInput).not.toBeNull(); if (!tokenInput) return; tokenInput.value = "abc123"; tokenInput.dispatchEvent(new Event("input", { bubbles: true })); - expect(onPatch).toHaveBeenCalledWith( - ["gateway", "auth", "token"], - "abc123", - ); + expect(onPatch).toHaveBeenCalledWith(["gateway", "auth", "token"], "abc123"); const tokenButton = Array.from( container.querySelectorAll(".cfg-segmented__btn"), @@ -76,9 +66,7 @@ describe("config form renderer", () => { tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(onPatch).toHaveBeenCalledWith(["mode"], "token"); - const checkbox = container.querySelector( - "input[type='checkbox']", - ) as HTMLInputElement | null; + const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement | null; expect(checkbox).not.toBeNull(); if (!checkbox) return; checkbox.checked = true; @@ -101,9 +89,7 @@ describe("config form renderer", () => { container, ); - const addButton = container.querySelector( - ".cfg-array__add", - ) as HTMLButtonElement | null; + const addButton = container.querySelector(".cfg-array__add") as HTMLButtonElement | null; expect(addButton).not.toBeUndefined(); addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]); diff --git a/ui/src/ui/controllers/assistant-identity.ts b/ui/src/ui/controllers/assistant-identity.ts index dd720efd3..98eb09087 100644 --- a/ui/src/ui/controllers/assistant-identity.ts +++ b/ui/src/ui/controllers/assistant-identity.ts @@ -1,8 +1,5 @@ import type { GatewayBrowserClient } from "../gateway"; -import { - normalizeAssistantIdentity, - type AssistantIdentity, -} from "../assistant-identity"; +import { normalizeAssistantIdentity, type AssistantIdentity } from "../assistant-identity"; export type AssistantIdentityState = { client: GatewayBrowserClient | null; diff --git a/ui/src/ui/controllers/chat.ts b/ui/src/ui/controllers/chat.ts index 518c35fe1..bc9cff6d5 100644 --- a/ui/src/ui/controllers/chat.ts +++ b/ui/src/ui/controllers/chat.ts @@ -144,9 +144,7 @@ export async function abortChatRun(state: ChatState): Promise { try { await state.client.request( "chat.abort", - runId - ? { sessionKey: state.sessionKey, runId } - : { sessionKey: state.sessionKey }, + runId ? { sessionKey: state.sessionKey, runId } : { sessionKey: state.sessionKey }, ); return true; } catch (err) { @@ -155,10 +153,7 @@ export async function abortChatRun(state: ChatState): Promise { } } -export function handleChatEvent( - state: ChatState, - payload?: ChatEventPayload, -) { +export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) { if (!payload) return null; if (payload.sessionKey !== state.sessionKey) return null; diff --git a/ui/src/ui/controllers/config.test.ts b/ui/src/ui/controllers/config.test.ts index e1bbb1c92..a7ae8f854 100644 --- a/ui/src/ui/controllers/config.test.ts +++ b/ui/src/ui/controllers/config.test.ts @@ -46,11 +46,11 @@ describe("applyConfigSnapshot", () => { config: { gateway: { mode: "remote", port: 9999 } }, valid: true, issues: [], - raw: "{\n \"gateway\": { \"mode\": \"remote\", \"port\": 9999 }\n}\n", + raw: '{\n "gateway": { "mode": "remote", "port": 9999 }\n}\n', }); expect(state.configRaw).toBe( - "{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n", + '{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n', ); }); @@ -129,7 +129,7 @@ describe("updateConfigFormValue", () => { updateConfigFormValue(state, ["gateway", "port"], 18789); expect(state.configRaw).toBe( - "{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n", + '{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n', ); }); }); @@ -142,7 +142,7 @@ describe("applyConfig", () => { state.client = { request } as unknown as ConfigState["client"]; state.applySessionKey = "agent:main:whatsapp:dm:+15555550123"; state.configFormMode = "raw"; - state.configRaw = "{\n agent: { workspace: \"~/clawd\" }\n}\n"; + state.configRaw = '{\n agent: { workspace: "~/clawd" }\n}\n'; state.configSnapshot = { hash: "hash-123", }; @@ -150,7 +150,7 @@ describe("applyConfig", () => { await applyConfig(state); expect(request).toHaveBeenCalledWith("config.apply", { - raw: "{\n agent: { workspace: \"~/clawd\" }\n}\n", + raw: '{\n agent: { workspace: "~/clawd" }\n}\n', baseHash: "hash-123", sessionKey: "agent:main:whatsapp:dm:+15555550123", }); diff --git a/ui/src/ui/controllers/config.ts b/ui/src/ui/controllers/config.ts index c66876eba..84b9ae515 100644 --- a/ui/src/ui/controllers/config.ts +++ b/ui/src/ui/controllers/config.ts @@ -1,9 +1,5 @@ import type { GatewayBrowserClient } from "../gateway"; -import type { - ConfigSchemaResponse, - ConfigSnapshot, - ConfigUiHints, -} from "../types"; +import type { ConfigSchemaResponse, ConfigSnapshot, ConfigUiHints } from "../types"; import { cloneConfigObject, removePathValue, @@ -57,10 +53,7 @@ export async function loadConfigSchema(state: ConfigState) { if (state.configSchemaLoading) return; state.configSchemaLoading = true; try { - const res = (await state.client.request( - "config.schema", - {}, - )) as ConfigSchemaResponse; + const res = (await state.client.request("config.schema", {})) as ConfigSchemaResponse; applyConfigSchema(state, res); } catch (err) { state.lastError = String(err); @@ -69,10 +62,7 @@ export async function loadConfigSchema(state: ConfigState) { } } -export function applyConfigSchema( - state: ConfigState, - res: ConfigSchemaResponse, -) { +export function applyConfigSchema(state: ConfigState, res: ConfigSchemaResponse) { state.configSchema = res.schema ?? null; state.configUiHints = res.uiHints ?? {}; state.configSchemaVersion = res.version ?? null; @@ -175,9 +165,7 @@ export function updateConfigFormValue( path: Array, value: unknown, ) { - const base = cloneConfigObject( - state.configForm ?? state.configSnapshot?.config ?? {}, - ); + const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {}); setPathValue(base, path, value); state.configForm = base; state.configFormDirty = true; @@ -186,13 +174,8 @@ export function updateConfigFormValue( } } -export function removeConfigFormValue( - state: ConfigState, - path: Array, -) { - const base = cloneConfigObject( - state.configForm ?? state.configSnapshot?.config ?? {}, - ); +export function removeConfigFormValue(state: ConfigState, path: Array) { + const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {}); removePathValue(base, path); state.configForm = base; state.configFormDirty = true; diff --git a/ui/src/ui/controllers/config/form-utils.ts b/ui/src/ui/controllers/config/form-utils.ts index fd40bb5ac..1edd97b9c 100644 --- a/ui/src/ui/controllers/config/form-utils.ts +++ b/ui/src/ui/controllers/config/form-utils.ts @@ -22,16 +22,14 @@ export function setPathValue( if (typeof key === "number") { if (!Array.isArray(current)) return; if (current[key] == null) { - current[key] = - typeof nextKey === "number" ? [] : ({} as Record); + current[key] = typeof nextKey === "number" ? [] : ({} as Record); } current = current[key] as Record | unknown[]; } else { if (typeof current !== "object" || current == null) return; const record = current as Record; if (record[key] == null) { - record[key] = - typeof nextKey === "number" ? [] : ({} as Record); + record[key] = typeof nextKey === "number" ? [] : ({} as Record); } current = record[key] as Record | unknown[]; } @@ -59,9 +57,7 @@ export function removePathValue( current = current[key] as Record | unknown[]; } else { if (typeof current !== "object" || current == null) return; - current = (current as Record)[key] as - | Record - | unknown[]; + current = (current as Record)[key] as Record | unknown[]; } if (current == null) return; } diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index d24e65936..718de062b 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -103,8 +103,7 @@ export async function addCronJob(state: CronState) { wakeMode: state.cronForm.wakeMode, payload, isolation: - state.cronForm.postToMainPrefix.trim() && - state.cronForm.sessionTarget === "isolated" + state.cronForm.postToMainPrefix.trim() && state.cronForm.sessionTarget === "isolated" ? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() } : undefined, }; @@ -125,11 +124,7 @@ export async function addCronJob(state: CronState) { } } -export async function toggleCronJob( - state: CronState, - job: CronJob, - enabled: boolean, -) { +export async function toggleCronJob(state: CronState, job: CronJob, enabled: boolean) { if (!state.client || !state.connected || state.cronBusy) return; state.cronBusy = true; state.cronError = null; diff --git a/ui/src/ui/controllers/debug.ts b/ui/src/ui/controllers/debug.ts index 5aa1eec43..2f189af88 100644 --- a/ui/src/ui/controllers/debug.ts +++ b/ui/src/ui/controllers/debug.ts @@ -29,9 +29,7 @@ export async function loadDebug(state: DebugState) { state.debugStatus = status as StatusSummary; state.debugHealth = health as HealthSnapshot; const modelPayload = models as { models?: unknown[] } | undefined; - state.debugModels = Array.isArray(modelPayload?.models) - ? modelPayload?.models - : []; + state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : []; state.debugHeartbeat = heartbeat as unknown; } catch (err) { state.debugCallError = String(err); diff --git a/ui/src/ui/controllers/devices.ts b/ui/src/ui/controllers/devices.ts index f08d7afb6..f42b4fefd 100644 --- a/ui/src/ui/controllers/devices.ts +++ b/ui/src/ui/controllers/devices.ts @@ -118,9 +118,7 @@ export async function revokeDeviceToken( params: { deviceId: string; role: string }, ) { if (!state.client || !state.connected) return; - const confirmed = window.confirm( - `Revoke token for ${params.deviceId} (${params.role})?`, - ); + const confirmed = window.confirm(`Revoke token for ${params.deviceId} (${params.role})?`); if (!confirmed) return; try { await state.client.request("device.token.revoke", params); diff --git a/ui/src/ui/controllers/exec-approval.ts b/ui/src/ui/controllers/exec-approval.ts index 60e0fc7a0..968b14efc 100644 --- a/ui/src/ui/controllers/exec-approval.ts +++ b/ui/src/ui/controllers/exec-approval.ts @@ -80,6 +80,9 @@ export function addExecApproval( return next; } -export function removeExecApproval(queue: ExecApprovalRequest[], id: string): ExecApprovalRequest[] { +export function removeExecApproval( + queue: ExecApprovalRequest[], + id: string, +): ExecApprovalRequest[] { return pruneExecApprovalQueue(queue).filter((entry) => entry.id !== id); } diff --git a/ui/src/ui/controllers/exec-approvals.ts b/ui/src/ui/controllers/exec-approvals.ts index ba938b9f3..87804642f 100644 --- a/ui/src/ui/controllers/exec-approvals.ts +++ b/ui/src/ui/controllers/exec-approvals.ts @@ -34,9 +34,7 @@ export type ExecApprovalsSnapshot = { file: ExecApprovalsFile; }; -export type ExecApprovalsTarget = - | { kind: "gateway" } - | { kind: "node"; nodeId: string }; +export type ExecApprovalsTarget = { kind: "gateway" } | { kind: "node"; nodeId: string }; export type ExecApprovalsState = { client: GatewayBrowserClient | null; @@ -120,10 +118,7 @@ export async function saveExecApprovals( state.lastError = "Exec approvals hash missing; reload and retry."; return; } - const file = - state.execApprovalsForm ?? - state.execApprovalsSnapshot?.file ?? - {}; + const file = state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {}; const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash }); if (!rpc) { state.lastError = "Select a node before saving exec approvals."; diff --git a/ui/src/ui/controllers/logs.ts b/ui/src/ui/controllers/logs.ts index 48f78dd23..662b5d7cb 100644 --- a/ui/src/ui/controllers/logs.ts +++ b/ui/src/ui/controllers/logs.ts @@ -16,14 +16,7 @@ export type LogsState = { }; const LOG_BUFFER_LIMIT = 2000; -const LEVELS = new Set([ - "trace", - "debug", - "info", - "warn", - "error", - "fatal", -]); +const LEVELS = new Set(["trace", "debug", "info", "warn", "error", "fatal"]); function parseMaybeJsonString(value: unknown) { if (typeof value !== "string") return null; @@ -53,11 +46,7 @@ export function parseLogLine(line: string): LogEntry { ? (obj._meta as Record) : null; const time = - typeof obj.time === "string" - ? obj.time - : typeof meta?.date === "string" - ? meta?.date - : null; + typeof obj.time === "string" ? obj.time : typeof meta?.date === "string" ? meta?.date : null; const level = normalizeLevel(meta?.logLevelName ?? meta?.level); const contextCandidate = @@ -94,17 +83,14 @@ export function parseLogLine(line: string): LogEntry { } } -export async function loadLogs( - state: LogsState, - opts?: { reset?: boolean; quiet?: boolean }, -) { +export async function loadLogs(state: LogsState, opts?: { reset?: boolean; quiet?: boolean }) { if (!state.client || !state.connected) return; if (state.logsLoading && !opts?.quiet) return; if (!opts?.quiet) state.logsLoading = true; state.logsError = null; try { const res = await state.client.request("logs.tail", { - cursor: opts?.reset ? undefined : state.logsCursor ?? undefined, + cursor: opts?.reset ? undefined : (state.logsCursor ?? undefined), limit: state.logsLimit, maxBytes: state.logsMaxBytes, }); diff --git a/ui/src/ui/controllers/nodes.ts b/ui/src/ui/controllers/nodes.ts index 2a6a8219d..b9255aaea 100644 --- a/ui/src/ui/controllers/nodes.ts +++ b/ui/src/ui/controllers/nodes.ts @@ -8,10 +8,7 @@ export type NodesState = { lastError: string | null; }; -export async function loadNodes( - state: NodesState, - opts?: { quiet?: boolean }, -) { +export async function loadNodes(state: NodesState, opts?: { quiet?: boolean }) { if (!state.client || !state.connected) return; if (state.nodesLoading) return; state.nodesLoading = true; diff --git a/ui/src/ui/controllers/presence.ts b/ui/src/ui/controllers/presence.ts index 67ac2761d..3dbec9061 100644 --- a/ui/src/ui/controllers/presence.ts +++ b/ui/src/ui/controllers/presence.ts @@ -17,9 +17,7 @@ export async function loadPresence(state: PresenceState) { state.presenceError = null; state.presenceStatus = null; try { - const res = (await state.client.request("system-presence", {})) as - | PresenceEntry[] - | undefined; + const res = (await state.client.request("system-presence", {})) as PresenceEntry[] | undefined; if (Array.isArray(res)) { state.presenceEntries = res; state.presenceStatus = res.length === 0 ? "No instances yet." : null; diff --git a/ui/src/ui/controllers/skills.ts b/ui/src/ui/controllers/skills.ts index 6e26a98ae..5708b12ef 100644 --- a/ui/src/ui/controllers/skills.ts +++ b/ui/src/ui/controllers/skills.ts @@ -45,9 +45,7 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions state.skillsLoading = true; state.skillsError = null; try { - const res = (await state.client.request("skills.status", {})) as - | SkillStatusReport - | undefined; + const res = (await state.client.request("skills.status", {})) as SkillStatusReport | undefined; if (res) state.skillsReport = res; } catch (err) { state.skillsError = getErrorMessage(err); @@ -56,19 +54,11 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions } } -export function updateSkillEdit( - state: SkillsState, - skillKey: string, - value: string, -) { +export function updateSkillEdit(state: SkillsState, skillKey: string, value: string) { state.skillEdits = { ...state.skillEdits, [skillKey]: value }; } -export async function updateSkillEnabled( - state: SkillsState, - skillKey: string, - enabled: boolean, -) { +export async function updateSkillEnabled(state: SkillsState, skillKey: string, enabled: boolean) { if (!state.client || !state.connected) return; state.skillsBusyKey = skillKey; state.skillsError = null; diff --git a/ui/src/ui/focus-mode.browser.test.ts b/ui/src/ui/focus-mode.browser.test.ts index ba81456bc..6f736dbf8 100644 --- a/ui/src/ui/focus-mode.browser.test.ts +++ b/ui/src/ui/focus-mode.browser.test.ts @@ -36,9 +36,7 @@ describe("chat focus mode", () => { expect(shell).not.toBeNull(); expect(shell?.classList.contains("shell--chat-focus")).toBe(false); - const toggle = app.querySelector( - 'button[title^="Toggle focus mode"]', - ); + const toggle = app.querySelector('button[title^="Toggle focus mode"]'); expect(toggle).not.toBeNull(); toggle?.click(); @@ -47,9 +45,7 @@ describe("chat focus mode", () => { const link = app.querySelector('a.nav-item[href="/channels"]'); expect(link).not.toBeNull(); - link?.dispatchEvent( - new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }), - ); + link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 })); await app.updateComplete; expect(app.tab).toBe("channels"); diff --git a/ui/src/ui/format.ts b/ui/src/ui/format.ts index 461195a7a..cdefd2f56 100644 --- a/ui/src/ui/format.ts +++ b/ui/src/ui/format.ts @@ -42,7 +42,10 @@ export function clampText(value: string, max = 120): string { return `${value.slice(0, Math.max(0, max - 1))}…`; } -export function truncateText(value: string, max: number): { +export function truncateText( + value: string, + max: number, +): { text: string; truncated: boolean; total: number; diff --git a/ui/src/ui/icons.ts b/ui/src/ui/icons.ts index eaf8f0e27..b6e9c6b85 100644 --- a/ui/src/ui/icons.ts +++ b/ui/src/ui/icons.ts @@ -5,40 +5,223 @@ import { html, type TemplateResult } from "lit"; export const icons = { // Navigation icons - messageSquare: html``, - barChart: html``, - link: html``, - radio: html``, - fileText: html``, - zap: html``, - monitor: html``, - settings: html``, - bug: html``, - scrollText: html``, - folder: html``, + messageSquare: html` + + + + `, + barChart: html` + + + + + + `, + link: html` + + + + + `, + radio: html` + + + + + `, + fileText: html` + + + + + + + + `, + zap: html` + + `, + monitor: html` + + + + + + `, + settings: html` + + + + + `, + bug: html` + + + + + + + + + + + + + + `, + scrollText: html` + + + + + + + `, + folder: html` + + + + `, // UI icons - menu: html``, - x: html``, - check: html``, - copy: html``, - search: html``, - brain: html``, - book: html``, - loader: html``, + menu: html` + + + + + + `, + x: html` + + + + + `, + check: html` + + `, + copy: html` + + + + + `, + search: html` + + + + + `, + brain: html` + + + + + + + + + + + + `, + book: html` + + + + `, + loader: html` + + + + + + + + + + + `, // Tool icons - wrench: html``, - fileCode: html``, - edit: html``, - penLine: html``, - paperclip: html``, - globe: html``, - image: html``, - smartphone: html``, - plug: html``, - circle: html``, - puzzle: html``, + wrench: html` + + + + `, + fileCode: html` + + + + + + + `, + edit: html` + + + + + `, + penLine: html` + + + + + `, + paperclip: html` + + + + `, + globe: html` + + + + + + `, + image: html` + + + + + + `, + smartphone: html` + + + + + `, + plug: html` + + + + + + + `, + circle: html` + + `, + puzzle: html` + + + + `, } as const; export type IconName = keyof typeof icons; @@ -52,7 +235,10 @@ export function renderIcon(name: IconName, className = "nav-item__icon"): Templa } // Legacy function for compatibility -export function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult { +export function renderEmojiIcon( + iconContent: string | TemplateResult, + className: string, +): TemplateResult { return html``; } diff --git a/ui/src/ui/navigation.browser.test.ts b/ui/src/ui/navigation.browser.test.ts index 0139ec552..c07d2bb79 100644 --- a/ui/src/ui/navigation.browser.test.ts +++ b/ui/src/ui/navigation.browser.test.ts @@ -75,13 +75,9 @@ describe("control UI routing", () => { const app = mountApp("/chat"); await app.updateComplete; - const link = app.querySelector( - 'a.nav-item[href="/channels"]', - ); + const link = app.querySelector('a.nav-item[href="/channels"]'); expect(link).not.toBeNull(); - link?.dispatchEvent( - new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }), - ); + link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 })); await app.updateComplete; expect(app.tab).toBe("channels"); diff --git a/ui/src/ui/navigation.ts b/ui/src/ui/navigation.ts index 5938e25e9..90df6bd57 100644 --- a/ui/src/ui/navigation.ts +++ b/ui/src/ui/navigation.ts @@ -37,9 +37,7 @@ const TAB_PATHS: Record = { logs: "/logs", }; -const PATH_TO_TAB = new Map( - Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]), -); +const PATH_TO_TAB = new Map(Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab])); export function normalizeBasePath(basePath: string): string { if (!basePath) return ""; diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index fc410c225..3245a4a86 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -49,22 +49,16 @@ export function loadSettings(): UiSettings { ? parsed.sessionKey.trim() : defaults.sessionKey, lastActiveSessionKey: - typeof parsed.lastActiveSessionKey === "string" && - parsed.lastActiveSessionKey.trim() + typeof parsed.lastActiveSessionKey === "string" && parsed.lastActiveSessionKey.trim() ? parsed.lastActiveSessionKey.trim() - : (typeof parsed.sessionKey === "string" && - parsed.sessionKey.trim()) || + : (typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()) || defaults.lastActiveSessionKey, theme: - parsed.theme === "light" || - parsed.theme === "dark" || - parsed.theme === "system" + parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system" ? parsed.theme : defaults.theme, chatFocusMode: - typeof parsed.chatFocusMode === "boolean" - ? parsed.chatFocusMode - : defaults.chatFocusMode, + typeof parsed.chatFocusMode === "boolean" ? parsed.chatFocusMode : defaults.chatFocusMode, chatShowThinking: typeof parsed.chatShowThinking === "boolean" ? parsed.chatShowThinking @@ -76,12 +70,9 @@ export function loadSettings(): UiSettings { ? parsed.splitRatio : defaults.splitRatio, navCollapsed: - typeof parsed.navCollapsed === "boolean" - ? parsed.navCollapsed - : defaults.navCollapsed, + typeof parsed.navCollapsed === "boolean" ? parsed.navCollapsed : defaults.navCollapsed, navGroupsCollapsed: - typeof parsed.navGroupsCollapsed === "object" && - parsed.navGroupsCollapsed !== null + typeof parsed.navGroupsCollapsed === "object" && parsed.navGroupsCollapsed !== null ? parsed.navGroupsCollapsed : defaults.navGroupsCollapsed, }; diff --git a/ui/src/ui/theme-transition.ts b/ui/src/ui/theme-transition.ts index b039c667b..10c3942c9 100644 --- a/ui/src/ui/theme-transition.ts +++ b/ui/src/ui/theme-transition.ts @@ -55,8 +55,7 @@ export const startThemeTransition = ({ const document_ = documentReference as DocumentWithViewTransition; const prefersReducedMotion = hasReducedMotionPreference(); - const canUseViewTransition = - Boolean(document_.startViewTransition) && !prefersReducedMotion; + const canUseViewTransition = Boolean(document_.startViewTransition) && !prefersReducedMotion; if (canUseViewTransition) { let xPercent = 0.5; @@ -71,11 +70,7 @@ export const startThemeTransition = ({ yPercent = clamp01(context.pointerClientY / window.innerHeight); } else if (context?.element) { const rect = context.element.getBoundingClientRect(); - if ( - rect.width > 0 && - rect.height > 0 && - typeof window !== "undefined" - ) { + if (rect.width > 0 && rect.height > 0 && typeof window !== "undefined") { xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth); yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight); } diff --git a/ui/src/ui/theme.ts b/ui/src/ui/theme.ts index 47bd5670b..4d2db4f27 100644 --- a/ui/src/ui/theme.ts +++ b/ui/src/ui/theme.ts @@ -5,9 +5,7 @@ export function getSystemTheme(): ResolvedTheme { if (typeof window === "undefined" || typeof window.matchMedia !== "function") { return "dark"; } - return window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } export function resolveTheme(mode: ThemeMode): ResolvedTheme { diff --git a/ui/src/ui/tool-display.json b/ui/src/ui/tool-display.json index 4a6bd0524..e4cea776e 100644 --- a/ui/src/ui/tool-display.json +++ b/ui/src/ui/tool-display.json @@ -90,7 +90,13 @@ }, "act": { "label": "act", - "detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"] + "detailKeys": [ + "request.kind", + "request.ref", + "request.selector", + "request.text", + "request.value" + ] } } }, @@ -117,9 +123,15 @@ "approve": { "label": "approve", "detailKeys": ["requestId"] }, "reject": { "label": "reject", "detailKeys": ["requestId"] }, "notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] }, - "camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] }, + "camera_snap": { + "label": "camera snap", + "detailKeys": ["node", "nodeId", "facing", "deviceId"] + }, "camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] }, - "camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] }, + "camera_clip": { + "label": "camera clip", + "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] + }, "screen_record": { "label": "screen record", "detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"] diff --git a/ui/src/ui/tool-display.ts b/ui/src/ui/tool-display.ts index 4b2de6ecb..7426e3371 100644 --- a/ui/src/ui/tool-display.ts +++ b/ui/src/ui/tool-display.ts @@ -153,8 +153,7 @@ export function resolveToolDisplay(params: { detail = resolveWriteDetail(params.args); } - const detailKeys = - actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? []; + const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? []; if (!detail && detailKeys.length > 0) { detail = resolveDetailFromKeys(params.args, detailKeys); } @@ -192,7 +191,5 @@ export function formatToolSummary(display: ToolDisplay): string { function shortenHomeInString(input: string): string { if (!input) return input; - return input - .replace(/\/Users\/[^/]+/g, "~") - .replace(/\/home\/[^/]+/g, "~"); + return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~"); } diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 1a5ec0731..b2ce740c5 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -514,13 +514,7 @@ export type StatusSummary = Record; export type HealthSnapshot = Record; -export type LogLevel = - | "trace" - | "debug" - | "info" - | "warn" - | "error" - | "fatal"; +export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; export type LogEntry = { raw: string; diff --git a/ui/src/ui/views/channels.config.ts b/ui/src/ui/views/channels.config.ts index 3c4d2c7df..73801b62d 100644 --- a/ui/src/ui/views/channels.config.ts +++ b/ui/src/ui/views/channels.config.ts @@ -2,12 +2,7 @@ import { html } from "lit"; import type { ConfigUiHints } from "../types"; import type { ChannelsProps } from "./channels.types"; -import { - analyzeConfigSchema, - renderNode, - schemaType, - type JsonSchema, -} from "./config-form"; +import { analyzeConfigSchema, renderNode, schemaType, type JsonSchema } from "./config-form"; type ChannelConfigFormProps = { channelId: string; @@ -61,9 +56,7 @@ function resolveChannelValue( (fromChannels && typeof fromChannels === "object" ? (fromChannels as Record) : null) ?? - (fallback && typeof fallback === "object" - ? (fallback as Record) - : null); + (fallback && typeof fallback === "object" ? (fallback as Record) : null); return resolved ?? {}; } @@ -71,11 +64,15 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) { const analysis = analyzeConfigSchema(props.schema); const normalized = analysis.schema; if (!normalized) { - return html`
Schema unavailable. Use Raw.
`; + return html` +
Schema unavailable. Use Raw.
+ `; } const node = resolveSchemaNode(normalized, ["channels", props.channelId]); if (!node) { - return html`
Channel config schema unavailable.
`; + return html` +
Channel config schema unavailable.
+ `; } const configValue = props.configValue ?? {}; const value = resolveChannelValue(configValue, props.channelId); @@ -95,24 +92,25 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) { `; } -export function renderChannelConfigSection(params: { - channelId: string; - props: ChannelsProps; -}) { +export function renderChannelConfigSection(params: { channelId: string; props: ChannelsProps }) { const { channelId, props } = params; const disabled = props.configSaving || props.configSchemaLoading; return html`
- ${props.configSchemaLoading - ? html`
Loading config schema…
` - : renderChannelConfigForm({ - channelId, - configValue: props.configForm, - schema: props.configSchema, - uiHints: props.configUiHints, - disabled, - onPatch: props.onConfigPatch, - })} + ${ + props.configSchemaLoading + ? html` +
Loading config schema…
+ ` + : renderChannelConfigForm({ + channelId, + configValue: props.configForm, + schema: props.configSchema, + uiHints: props.configUiHints, + disabled, + onPatch: props.onConfigPatch, + }) + }
- ${discord?.lastError - ? html`
+ ${ + discord?.lastError + ? html`
${discord.lastError}
` - : nothing} + : nothing + } - ${discord?.probe - ? html`
+ ${ + discord?.probe + ? html`
Probe ${discord.probe.ok ? "ok" : "failed"} · ${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "discord", props })} diff --git a/ui/src/ui/views/channels.googlechat.ts b/ui/src/ui/views/channels.googlechat.ts index a014ac89e..90c0eaec4 100644 --- a/ui/src/ui/views/channels.googlechat.ts +++ b/ui/src/ui/views/channels.googlechat.ts @@ -34,9 +34,11 @@ export function renderGoogleChatCard(params: {
Audience - ${googleChat?.audienceType - ? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}` - : "n/a"} + ${ + googleChat?.audienceType + ? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}` + : "n/a" + }
@@ -49,18 +51,22 @@ export function renderGoogleChatCard(params: {
- ${googleChat?.lastError - ? html`
+ ${ + googleChat?.lastError + ? html`
${googleChat.lastError}
` - : nothing} + : nothing + } - ${googleChat?.probe - ? html`
+ ${ + googleChat?.probe + ? html`
Probe ${googleChat.probe.ok ? "ok" : "failed"} · ${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "googlechat", props })} diff --git a/ui/src/ui/views/channels.imessage.ts b/ui/src/ui/views/channels.imessage.ts index 85fd90d03..ca468f978 100644 --- a/ui/src/ui/views/channels.imessage.ts +++ b/ui/src/ui/views/channels.imessage.ts @@ -37,18 +37,22 @@ export function renderIMessageCard(params: {
- ${imessage?.lastError - ? html`
+ ${ + imessage?.lastError + ? html`
${imessage.lastError}
` - : nothing} + : nothing + } - ${imessage?.probe - ? html`
+ ${ + imessage?.probe + ? html`
Probe ${imessage.probe.ok ? "ok" : "failed"} · ${imessage.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "imessage", props })} diff --git a/ui/src/ui/views/channels.nostr-profile-form.ts b/ui/src/ui/views/channels.nostr-profile-form.ts index 8565d8ef9..36231e184 100644 --- a/ui/src/ui/views/channels.nostr-profile-form.ts +++ b/ui/src/ui/views/channels.nostr-profile-form.ts @@ -82,7 +82,7 @@ export function renderNostrProfileForm(params: { placeholder?: string; maxLength?: number; help?: string; - } = {} + } = {}, ) => { const { type = "text", placeholder, maxLength, help } = opts; const value = state.values[field] ?? ""; @@ -169,13 +169,17 @@ export function renderNostrProfileForm(params: {
Account: ${accountId}
- ${state.error - ? html`
${state.error}
` - : nothing} + ${ + state.error + ? html`
${state.error}
` + : nothing + } - ${state.success - ? html`
${state.success}
` - : nothing} + ${ + state.success + ? html`
${state.success}
` + : nothing + } ${renderPicturePreview()} @@ -204,8 +208,9 @@ export function renderNostrProfileForm(params: { help: "HTTPS URL to your profile picture", })} - ${state.showAdvanced - ? html` + ${ + state.showAdvanced + ? html`
Advanced
@@ -232,7 +237,8 @@ export function renderNostrProfileForm(params: { })}
` - : nothing} + : nothing + }
- ${isDirty - ? html`
- You have unsaved changes -
` - : nothing} + ${ + isDirty + ? html` +
+ You have unsaved changes +
+ ` + : nothing + }
`; } @@ -284,7 +294,7 @@ export function renderNostrProfileForm(params: { * Create initial form state from existing profile */ export function createNostrProfileFormState( - profile: NostrProfileType | undefined + profile: NostrProfileType | undefined, ): NostrProfileFormState { const values: NostrProfileType = { name: profile?.name ?? "", @@ -305,8 +315,6 @@ export function createNostrProfileFormState( error: null, success: null, fieldErrors: {}, - showAdvanced: Boolean( - profile?.banner || profile?.website || profile?.nip05 || profile?.lud16 - ), + showAdvanced: Boolean(profile?.banner || profile?.website || profile?.nip05 || profile?.lud16), }; } diff --git a/ui/src/ui/views/channels.nostr.ts b/ui/src/ui/views/channels.nostr.ts index 05152d80b..1a4a2cb09 100644 --- a/ui/src/ui/views/channels.nostr.ts +++ b/ui/src/ui/views/channels.nostr.ts @@ -44,8 +44,7 @@ export function renderNostrCard(params: { const summaryConfigured = nostr?.configured ?? primaryAccount?.configured ?? false; const summaryRunning = nostr?.running ?? primaryAccount?.running ?? false; const summaryPublicKey = - nostr?.publicKey ?? - (primaryAccount as { publicKey?: string } | undefined)?.publicKey; + nostr?.publicKey ?? (primaryAccount as { publicKey?: string } | undefined)?.publicKey; const summaryLastStartAt = nostr?.lastStartAt ?? primaryAccount?.lastStartAt ?? null; const summaryLastError = nostr?.lastError ?? primaryAccount?.lastError ?? null; const hasMultipleAccounts = nostrAccounts.length > 1; @@ -79,11 +78,13 @@ export function renderNostrCard(params: { Last inbound ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
- ${account.lastError - ? html` + ${ + account.lastError + ? html` ` - : nothing} + : nothing + }
`; @@ -100,17 +101,19 @@ export function renderNostrCard(params: { } const profile = - (primaryAccount as - | { - profile?: { - name?: string; - displayName?: string; - about?: string; - picture?: string; - nip05?: string; - }; - } - | undefined)?.profile ?? nostr?.profile; + ( + primaryAccount as + | { + profile?: { + name?: string; + displayName?: string; + about?: string; + picture?: string; + nip05?: string; + }; + } + | undefined + )?.profile ?? nostr?.profile; const { name, displayName, about, picture, nip05 } = profile ?? {}; const hasAnyProfileData = name || displayName || about || picture || nip05; @@ -118,8 +121,9 @@ export function renderNostrCard(params: {
Profile
- ${summaryConfigured - ? html` + ${ + summaryConfigured + ? html`
- ${hasAnyProfileData - ? html` + ${ + hasAnyProfileData + ? html`
- ${picture - ? html` + ${ + picture + ? html`
` - : nothing} + : nothing + } ${name ? html`
Name${name}
` : nothing} - ${displayName - ? html`
Display Name${displayName}
` - : nothing} - ${about - ? html`
About${about}
` - : nothing} + ${ + displayName + ? html`
Display Name${displayName}
` + : nothing + } + ${ + about + ? html`
About${about}
` + : nothing + } ${nip05 ? html`
NIP-05${nip05}
` : nothing}
` - : html` -
- No profile set. Click "Edit Profile" to add your name, bio, and avatar. -
- `} + : html` +
+ No profile set. Click "Edit Profile" to add your name, bio, and avatar. +
+ ` + }
`; }; @@ -172,13 +185,14 @@ export function renderNostrCard(params: {
Decentralized DMs via Nostr relays (NIP-04).
${accountCountLabel} - ${hasMultipleAccounts - ? html` + ${ + hasMultipleAccounts + ? html` ` - : html` + : html`
Configured @@ -199,11 +213,14 @@ export function renderNostrCard(params: { ${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}
- `} + ` + } - ${summaryLastError - ? html`
${summaryLastError}
` - : nothing} + ${ + summaryLastError + ? html`
${summaryLastError}
` + : nothing + } ${renderProfileSection()} diff --git a/ui/src/ui/views/channels.signal.ts b/ui/src/ui/views/channels.signal.ts index 9d4f6c147..b2bbbae78 100644 --- a/ui/src/ui/views/channels.signal.ts +++ b/ui/src/ui/views/channels.signal.ts @@ -41,18 +41,22 @@ export function renderSignalCard(params: { - ${signal?.lastError - ? html`
+ ${ + signal?.lastError + ? html`
${signal.lastError}
` - : nothing} + : nothing + } - ${signal?.probe - ? html`
+ ${ + signal?.probe + ? html`
Probe ${signal.probe.ok ? "ok" : "failed"} · ${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "signal", props })} diff --git a/ui/src/ui/views/channels.slack.ts b/ui/src/ui/views/channels.slack.ts index eb93ac4c3..a2ee1b227 100644 --- a/ui/src/ui/views/channels.slack.ts +++ b/ui/src/ui/views/channels.slack.ts @@ -37,18 +37,22 @@ export function renderSlackCard(params: {
- ${slack?.lastError - ? html`
+ ${ + slack?.lastError + ? html`
${slack.lastError}
` - : nothing} + : nothing + } - ${slack?.probe - ? html`
+ ${ + slack?.probe + ? html`
Probe ${slack.probe.ok ? "ok" : "failed"} · ${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "slack", props })} diff --git a/ui/src/ui/views/channels.telegram.ts b/ui/src/ui/views/channels.telegram.ts index 498d98f87..097f89fed 100644 --- a/ui/src/ui/views/channels.telegram.ts +++ b/ui/src/ui/views/channels.telegram.ts @@ -39,13 +39,15 @@ export function renderTelegramCard(params: { Last inbound ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
- ${account.lastError - ? html` + ${ + account.lastError + ? html` ` - : nothing} + : nothing + }
`; @@ -57,13 +59,14 @@ export function renderTelegramCard(params: {
Bot status and channel configuration.
${accountCountLabel} - ${hasMultipleAccounts - ? html` + ${ + hasMultipleAccounts + ? html` ` - : html` + : html`
Configured @@ -86,20 +89,25 @@ export function renderTelegramCard(params: { ${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}
- `} + ` + } - ${telegram?.lastError - ? html`
+ ${ + telegram?.lastError + ? html`
${telegram.lastError}
` - : nothing} + : nothing + } - ${telegram?.probe - ? html`
+ ${ + telegram?.probe + ? html`
Probe ${telegram.probe.ok ? "ok" : "failed"} · ${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "telegram", props })} diff --git a/ui/src/ui/views/channels.ts b/ui/src/ui/views/channels.ts index a0fce8f40..78bb52983 100644 --- a/ui/src/ui/views/channels.ts +++ b/ui/src/ui/views/channels.ts @@ -15,11 +15,7 @@ import type { TelegramStatus, WhatsAppStatus, } from "../types"; -import type { - ChannelKey, - ChannelsChannelData, - ChannelsProps, -} from "./channels.types"; +import type { ChannelKey, ChannelsChannelData, ChannelsProps } from "./channels.types"; import { channelEnabled, renderChannelAccountCount } from "./channels.shared"; import { renderChannelConfigSection } from "./channels.config"; import { renderDiscordCard } from "./channels.discord"; @@ -33,12 +29,8 @@ import { renderWhatsAppCard } from "./channels.whatsapp"; export function renderChannels(props: ChannelsProps) { const channels = props.snapshot?.channels as Record | null; - const whatsapp = (channels?.whatsapp ?? undefined) as - | WhatsAppStatus - | undefined; - const telegram = (channels?.telegram ?? undefined) as - | TelegramStatus - | undefined; + const whatsapp = (channels?.whatsapp ?? undefined) as WhatsAppStatus | undefined; + const telegram = (channels?.telegram ?? undefined) as TelegramStatus | undefined; const discord = (channels?.discord ?? null) as DiscordStatus | null; const googlechat = (channels?.googlechat ?? null) as GoogleChatStatus | null; const slack = (channels?.slack ?? null) as SlackStatus | null; @@ -82,11 +74,13 @@ export function renderChannels(props: ChannelsProps) {
${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}
- ${props.lastError - ? html`
+ ${ + props.lastError + ? html`
${props.lastError}
` - : nothing} + : nothing + }
 ${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
       
@@ -101,27 +95,11 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe if (snapshot?.channelOrder?.length) { return snapshot.channelOrder; } - return [ - "whatsapp", - "telegram", - "discord", - "googlechat", - "slack", - "signal", - "imessage", - "nostr", - ]; + return ["whatsapp", "telegram", "discord", "googlechat", "slack", "signal", "imessage", "nostr"]; } -function renderChannel( - key: ChannelKey, - props: ChannelsProps, - data: ChannelsChannelData, -) { - const accountCountLabel = renderChannelAccountCount( - key, - data.channelAccounts, - ); +function renderChannel(key: ChannelKey, props: ChannelsProps, data: ChannelsChannelData) { + const accountCountLabel = renderChannelAccountCount(key, data.channelAccounts); switch (key) { case "whatsapp": return renderWhatsAppCard({ @@ -218,13 +196,14 @@ function renderGenericChannelCard(
Channel status and configuration.
${accountCountLabel} - ${accounts.length > 0 - ? html` + ${ + accounts.length > 0 + ? html` ` - : html` + : html`
Configured @@ -239,13 +218,16 @@ function renderGenericChannelCard( ${connected == null ? "n/a" : connected ? "Yes" : "No"}
- `} + ` + } - ${lastError - ? html`
+ ${ + lastError + ? html`
${lastError}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: key, props })}
@@ -259,10 +241,7 @@ function resolveChannelMetaMap( return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry])); } -function resolveChannelLabel( - snapshot: ChannelsStatusSnapshot | null, - key: string, -): string { +function resolveChannelLabel(snapshot: ChannelsStatusSnapshot | null, key: string): string { const meta = resolveChannelMetaMap(snapshot)[key]; return meta?.label ?? snapshot?.channelLabels?.[key] ?? key; } @@ -316,13 +295,15 @@ function renderGenericAccount(account: ChannelAccountSnapshot) { Last inbound ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
- ${account.lastError - ? html` + ${ + account.lastError + ? html` ` - : nothing} + : nothing + } `; diff --git a/ui/src/ui/views/channels.types.ts b/ui/src/ui/views/channels.types.ts index b69507bf3..fa1a9094b 100644 --- a/ui/src/ui/views/channels.types.ts +++ b/ui/src/ui/views/channels.types.ts @@ -1,16 +1,16 @@ import type { - ChannelAccountSnapshot, - ChannelsStatusSnapshot, - ConfigUiHints, - DiscordStatus, - GoogleChatStatus, - IMessageStatus, - NostrProfile, - NostrStatus, - SignalStatus, - SlackStatus, - TelegramStatus, - WhatsAppStatus, + ChannelAccountSnapshot, + ChannelsStatusSnapshot, + ConfigUiHints, + DiscordStatus, + GoogleChatStatus, + IMessageStatus, + NostrProfile, + NostrStatus, + SignalStatus, + SlackStatus, + TelegramStatus, + WhatsAppStatus, } from "../types"; import type { NostrProfileFormState } from "./channels.nostr-profile-form"; diff --git a/ui/src/ui/views/channels.whatsapp.ts b/ui/src/ui/views/channels.whatsapp.ts index eae3be695..f18bb9b7c 100644 --- a/ui/src/ui/views/channels.whatsapp.ts +++ b/ui/src/ui/views/channels.whatsapp.ts @@ -39,9 +39,7 @@ export function renderWhatsAppCard(params: {
Last connect - ${whatsapp?.lastConnectedAt - ? formatAgo(whatsapp.lastConnectedAt) - : "n/a"} + ${whatsapp?.lastConnectedAt ? formatAgo(whatsapp.lastConnectedAt) : "n/a"}
@@ -53,30 +51,34 @@ export function renderWhatsAppCard(params: {
Auth age - ${whatsapp?.authAgeMs != null - ? formatDuration(whatsapp.authAgeMs) - : "n/a"} + ${whatsapp?.authAgeMs != null ? formatDuration(whatsapp.authAgeMs) : "n/a"}
- ${whatsapp?.lastError - ? html`
+ ${ + whatsapp?.lastError + ? html`
${whatsapp.lastError}
` - : nothing} + : nothing + } - ${props.whatsappMessage - ? html`
+ ${ + props.whatsappMessage + ? html`
${props.whatsappMessage}
` - : nothing} + : nothing + } - ${props.whatsappQrDataUrl - ? html`
+ ${ + props.whatsappQrDataUrl + ? html`
WhatsApp QR
` - : nothing} + : nothing + }
`; } @@ -89,7 +86,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un if (elapsed < COMPACTION_TOAST_DURATION_MS) { return html`
- ${icons.check} Context compacted + 🧹 Context compacted
`; } @@ -178,9 +175,7 @@ export function renderChat(props: ChatProps) { const canCompose = props.connected; const isBusy = props.sending || props.stream !== null; const canAbort = Boolean(props.canAbort && props.onAbort); - const activeSession = props.sessions?.sessions?.find( - (row) => row.key === props.sessionKey, - ); + const activeSession = props.sessions?.sessions?.find((row) => row.key === props.sessionKey); const reasoningLevel = activeSession?.reasoningLevel ?? "off"; const showReasoning = props.showThinking && reasoningLevel !== "off"; const assistantIdentity = { @@ -204,49 +199,56 @@ export function renderChat(props: ChatProps) { aria-live="polite" @scroll=${props.onChatScroll} > - ${props.loading ? html`
Loading chat…
` : nothing} - ${repeat(buildChatItems(props), (item) => item.key, (item) => { - if (item.kind === "reading-indicator") { - return renderReadingIndicatorGroup(assistantIdentity); - } + ${ + props.loading + ? html` +
Loading chat…
+ ` + : nothing + } + ${repeat( + buildChatItems(props), + (item) => item.key, + (item) => { + if (item.kind === "reading-indicator") { + return renderReadingIndicatorGroup(assistantIdentity); + } - if (item.kind === "stream") { - return renderStreamingGroup( - item.text, - item.startedAt, - props.onOpenSidebar, - assistantIdentity, - ); - } + if (item.kind === "stream") { + return renderStreamingGroup( + item.text, + item.startedAt, + props.onOpenSidebar, + assistantIdentity, + ); + } - if (item.kind === "group") { - return renderMessageGroup(item, { - onOpenSidebar: props.onOpenSidebar, - showReasoning, - assistantName: props.assistantName, - assistantAvatar: assistantIdentity.avatar, - }); - } + if (item.kind === "group") { + return renderMessageGroup(item, { + onOpenSidebar: props.onOpenSidebar, + showReasoning, + assistantName: props.assistantName, + assistantAvatar: assistantIdentity.avatar, + }); + } - return nothing; - })} + return nothing; + }, + )}
`; return html`
- ${props.disabledReason - ? html`
${props.disabledReason}
` - : nothing} + ${props.disabledReason ? html`
${props.disabledReason}
` : nothing} - ${props.error - ? html`
${props.error}
` - : nothing} + ${props.error ? html`
${props.error}
` : nothing} ${renderCompactionIndicator(props.compactionStatus)} - ${props.focusMode - ? html` + ${ + props.focusMode + ? html` ` - : nothing} + : nothing + }
- ${sidebarOpen - ? html` + ${ + sidebarOpen + ? html` - props.onSplitRatioChange?.(e.detail.splitRatio)} + @resize=${(e: CustomEvent) => props.onSplitRatioChange?.(e.detail.splitRatio)} >
${renderMarkdownSidebar({ @@ -288,11 +291,13 @@ export function renderChat(props: ChatProps) { })}
` - : nothing} + : nothing + }
- ${props.queue.length - ? html` + ${ + props.queue.length + ? html`
Queued (${props.queue.length})
@@ -311,7 +316,7 @@ export function renderChat(props: ChatProps) { aria-label="Remove queued message" @click=${() => props.onQueueRemove(item.id)} > - ${icons.x} + ✕
`, @@ -319,7 +324,8 @@ export function renderChat(props: ChatProps) {
` - : nothing} + : nothing + }
${renderAttachmentPreview(props)} diff --git a/ui/src/ui/views/config-form.analyze.ts b/ui/src/ui/views/config-form.analyze.ts index 5e72ffff1..a6451806c 100644 --- a/ui/src/ui/views/config-form.analyze.ts +++ b/ui/src/ui/views/config-form.analyze.ts @@ -47,8 +47,7 @@ function normalizeSchemaNode( const nullable = Array.isArray(schema.type) && schema.type.includes("null"); const type = - schemaType(schema) ?? - (schema.properties || schema.additionalProperties ? "object" : undefined); + schemaType(schema) ?? (schema.properties || schema.additionalProperties ? "object" : undefined); normalized.type = type ?? schema.type; normalized.nullable = nullable || schema.nullable; @@ -73,24 +72,15 @@ function normalizeSchemaNode( unsupported.add(pathLabel); } else if (schema.additionalProperties === false) { normalized.additionalProperties = false; - } else if ( - schema.additionalProperties && - typeof schema.additionalProperties === "object" - ) { + } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") { if (!isAnySchema(schema.additionalProperties as JsonSchema)) { - const res = normalizeSchemaNode( - schema.additionalProperties as JsonSchema, - [...path, "*"], - ); - normalized.additionalProperties = - res.schema ?? (schema.additionalProperties as JsonSchema); + const res = normalizeSchemaNode(schema.additionalProperties as JsonSchema, [...path, "*"]); + normalized.additionalProperties = res.schema ?? (schema.additionalProperties as JsonSchema); if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel); } } } else if (type === "array") { - const itemsSchema = Array.isArray(schema.items) - ? schema.items[0] - : schema.items; + const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items; if (!itemsSchema) { unsupported.add(pathLabel); } else { diff --git a/ui/src/ui/views/config-form.node.ts b/ui/src/ui/views/config-form.node.ts index 9d121d7f1..5812a6f30 100644 --- a/ui/src/ui/views/config-form.node.ts +++ b/ui/src/ui/views/config-form.node.ts @@ -28,11 +28,69 @@ function jsonValue(value: unknown): string { // SVG Icons as template literals const icons = { - chevronDown: html``, - plus: html``, - minus: html``, - trash: html``, - edit: html``, + chevronDown: html` + + + + `, + plus: html` + + + + + `, + minus: html` + + + + `, + trash: html` + + + + + `, + edit: html` + + + + + `, }; export function renderNode(params: { @@ -64,7 +122,7 @@ export function renderNode(params: { if (schema.anyOf || schema.oneOf) { const variants = schema.anyOf ?? schema.oneOf ?? []; const nonNull = variants.filter( - (v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))) + (v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))), ); if (nonNull.length === 1) { @@ -88,16 +146,18 @@ export function renderNode(params: { ${showLabel ? html`` : nothing} ${help ? html`
${help}
` : nothing}
- ${literals.map((lit, idx) => html` + ${literals.map( + (lit) => html` - `)} + `, + )}
`; @@ -109,11 +169,9 @@ export function renderNode(params: { } // Handle mixed primitive types - const primitiveTypes = new Set( - nonNull.map((variant) => schemaType(variant)).filter(Boolean) - ); + const primitiveTypes = new Set(nonNull.map((variant) => schemaType(variant)).filter(Boolean)); const normalizedTypes = new Set( - [...primitiveTypes].map((v) => (v === "integer" ? "number" : v)) + [...primitiveTypes].map((v) => (v === "integer" ? "number" : v)), ); if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) { @@ -147,16 +205,18 @@ export function renderNode(params: { ${showLabel ? html`` : nothing} ${help ? html`
${help}
` : nothing}
- ${options.map((opt) => html` + ${options.map( + (opt) => html` - `)} + `, + )}
`; @@ -176,9 +236,14 @@ export function renderNode(params: { // Boolean - toggle row if (type === "boolean") { - const displayValue = typeof value === "boolean" ? value : typeof schema.default === "boolean" ? schema.default : false; + const displayValue = + typeof value === "boolean" + ? value + : typeof schema.default === "boolean" + ? schema.default + : false; return html` -