diff --git a/.gitignore b/.gitignore index 9d31b8c8604..0eabcb6843c 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,7 @@ docs/superpowers/specs/2026-03-10-collapsed-side-nav-design.md .gitignore test/config-form.analyze.telegram.test.ts ui/src/ui/theme-variants.browser.test.ts -ui/src/ui/__screenshots__/navigation.browser.test.ts/control-UI-routing-auto-scrolls-chat-history-to-the-latest-message-1.png -ui/src/ui/__screenshots__/navigation.browser.test.ts/control-UI-routing-auto-scrolls-chat-history-to-the-latest-message-1.png -ui/src/ui/__screenshots__/navigation.browser.test.ts/control-UI-routing-auto-scrolls-chat-history-to-the-latest-message-1.png +ui/src/ui/__screenshots__ +ui/src/ui/views/__screenshots__ +ui/.vitest-attachments +docs/superpowers diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index cd482f46f7c..9955557b886 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -401,7 +401,6 @@ img.chat-avatar { .chat-delete-confirm { position: absolute; bottom: calc(100% + 6px); - left: 0; background: var(--card, #1a1a1a); border: 1px solid var(--border, rgba(255, 255, 255, 0.1)); border-radius: var(--radius-md, 8px); @@ -412,6 +411,14 @@ img.chat-avatar { animation: scale-in 0.15s ease-out; } +.chat-delete-confirm--left { + right: 0; +} + +.chat-delete-confirm--right { + left: 0; +} + .chat-delete-confirm__text { margin: 0 0 8px; font-size: 13px; diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 6d12698d6b2..536acddd29e 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -670,6 +670,18 @@ max-width: 300px; } +.chat-controls__session-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.chat-controls__model { + min-width: 170px; + max-width: 320px; +} + .chat-controls__thinking { display: flex; align-items: center; @@ -760,6 +772,10 @@ text-overflow: ellipsis; } +.chat-controls__model select { + max-width: 320px; +} + .chat-controls__thinking { display: flex; align-items: center; @@ -812,6 +828,10 @@ .chat-controls__session { min-width: 120px; } + + .chat-controls__model { + min-width: 150px; + } } /* Chat loading skeleton */ diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 2114ea2565b..12f22aef21d 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -5,8 +5,8 @@ .shell { --shell-pad: 16px; --shell-gap: 16px; - --shell-nav-width: 220px; - --shell-nav-rail-width: 72px; + --shell-nav-width: 288px; + --shell-nav-rail-width: 78px; --shell-topbar-height: 52px; --shell-focus-duration: 200ms; --shell-focus-ease: var(--ease-out); @@ -15,7 +15,7 @@ grid-template-columns: var(--shell-nav-width) minmax(0, 1fr); grid-template-rows: var(--shell-topbar-height) 1fr; grid-template-areas: - "topbar topbar" + "nav topbar" "nav content"; gap: 0; animation: dashboard-enter 0.3s var(--ease-out); @@ -50,6 +50,7 @@ } .shell--onboarding { + grid-template-columns: 0 minmax(0, 1fr); grid-template-rows: 0 1fr; } @@ -57,6 +58,10 @@ display: none; } +.shell--onboarding .shell-nav { + display: none; +} + .shell--onboarding .content { padding-top: 0; } @@ -79,21 +84,42 @@ top: 0; z-index: 40; display: flex; - justify-content: space-between; align-items: center; - gap: 16px; - padding: 0 20px; - height: var(--shell-topbar-height); - border-bottom: 1px solid var(--border); - background: color-mix(in srgb, var(--bg) 85%, transparent); + padding: 0 24px; + min-height: 58px; + border-bottom: 1px solid color-mix(in srgb, var(--border) 74%, transparent); + background: color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter: blur(12px) saturate(1.6); -webkit-backdrop-filter: blur(12px) saturate(1.6); } -.topbar-left { +.topnav-shell { + display: flex; + align-items: center; + gap: 16px; + width: 100%; + min-height: var(--shell-topbar-height); + padding: 0; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.topbar-nav-toggle { + display: none; +} + +.topnav-shell__actions { display: flex; align-items: center; gap: 12px; + flex-shrink: 0; +} + +.topnav-shell__content { + min-width: 0; + flex: 1; } .topbar .nav-collapse-toggle { @@ -112,49 +138,36 @@ height: 20px; } -/* Brand */ -.brand { +.topnav-shell .dashboard-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; +} + +.topnav-shell .dashboard-header__breadcrumb { display: flex; align-items: center; gap: 8px; + min-width: 0; + overflow: hidden; + font-size: 13px; } -.brand-logo { - width: 26px; - height: 26px; - flex-shrink: 0; -} - -.brand-logo img { - width: 100%; - height: 100%; - object-fit: contain; -} - -.brand-text { - display: flex; - flex-direction: column; - gap: 0; -} - -.brand-title { - font-size: 15px; - font-weight: 700; - letter-spacing: -0.03em; - line-height: 1.1; - color: var(--text-strong); -} - -.brand-sub { - font-size: 9px; - font-weight: 500; +.topnav-shell .dashboard-header__breadcrumb-link, +.topnav-shell .dashboard-header__breadcrumb-sep { color: var(--muted); - letter-spacing: 0.06em; - text-transform: uppercase; - line-height: 1; } -/* Topbar status */ +.topnav-shell .dashboard-header__breadcrumb-current { + color: var(--text-strong); + font-weight: 650; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .topbar-status { display: flex; align-items: center; @@ -188,15 +201,15 @@ font-size: 13px; } -/* Topbar search trigger */ .topbar-search { display: inline-flex; align-items: center; gap: 12px; - padding: 7px 12px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); + min-height: 38px; + padding: 0 14px; + border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-elevated) 84%, transparent); color: var(--muted); font-size: 13px; cursor: pointer; @@ -204,12 +217,12 @@ border-color var(--duration-fast) ease, background var(--duration-fast) ease, color var(--duration-fast) ease; - min-width: 180px; + min-width: 200px; } .topbar-search:hover { - border-color: var(--border-strong); - background: var(--bg-hover); + border-color: color-mix(in srgb, var(--border-strong) 90%, transparent); + background: color-mix(in srgb, var(--bg-hover) 84%, transparent); color: var(--text); } @@ -242,9 +255,9 @@ align-items: center; gap: 2px; padding: 3px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--bg-elevated) 70%, transparent); + border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-elevated) 78%, transparent); } .topbar-theme-mode__btn { @@ -292,19 +305,22 @@ } /* =========================================== - Navigation Sidebar (shadcn-inspired) + Navigation Sidebar =========================================== */ -/* Sidebar wrapper – occupies the "nav" grid area */ .shell-nav { grid-area: nav; display: flex; - min-height: 0; + min-height: 100%; overflow: hidden; + border-right: 1px solid color-mix(in srgb, var(--border) 74%, transparent); transition: width var(--shell-focus-duration) var(--shell-focus-ease); } -/* The sidebar panel itself */ +.shell-nav-backdrop { + display: none; +} + .sidebar { display: flex; flex-direction: column; @@ -312,67 +328,103 @@ min-height: 0; min-width: 0; overflow: hidden; - background: var(--bg); + background: color-mix(in srgb, var(--bg) 96%, var(--bg-elevated) 4%); } :root[data-theme-mode="light"] .sidebar { - background: var(--panel); + background: color-mix(in srgb, var(--panel) 98%, white 2%); +} + +.sidebar-shell { + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; + padding: 14px 14px 12px; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; } -/* Collapsed: icon-only rail */ .sidebar--collapsed { width: var(--shell-nav-rail-width); min-width: var(--shell-nav-rail-width); flex: 0 0 var(--shell-nav-rail-width); - border-right: 1px solid color-mix(in srgb, var(--border-strong) 72%, transparent); } -/* Header: brand + collapse toggle */ -.sidebar-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 14px 14px 6px; +.sidebar-shell__header, +.sidebar-shell__footer { flex-shrink: 0; } -.sidebar--collapsed .sidebar-header { - justify-content: center; - padding: 12px 10px 6px; +.sidebar-shell__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 0; + padding: 0 8px 18px; +} + +.sidebar-shell__body { + min-height: 0; + flex: 1; + display: flex; +} + +.sidebar-shell__footer { + padding: 12px 8px 0; + border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent); } -/* Brand lockup */ .sidebar-brand { display: flex; align-items: center; - gap: 8px; + gap: 10px; min-width: 0; } .sidebar-brand__logo { - width: 22px; - height: 22px; + width: 32px; + height: 32px; flex-shrink: 0; - border-radius: 6px; + border-radius: 10px; + box-shadow: 0 8px 18px color-mix(in srgb, black 12%, transparent); +} + +.sidebar-brand__copy { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.sidebar-brand__eyebrow { + font-size: 10px; + line-height: 1.1; + font-weight: 600; + letter-spacing: 0.08em; + color: var(--muted); + text-transform: uppercase; } .sidebar-brand__title { - font-size: 14px; + font-size: 15px; + line-height: 1.1; font-weight: 700; - letter-spacing: -0.025em; + letter-spacing: -0.03em; color: var(--text-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -/* Scrollable nav body */ .sidebar-nav { flex: 1; overflow-y: auto; overflow-x: hidden; - padding: 4px 8px; + padding: 0; scrollbar-width: none; } @@ -380,177 +432,31 @@ display: none; } -.sidebar--collapsed .sidebar-nav { - padding: 4px 8px; - display: flex; - flex-direction: column; - gap: 24px; -} - -/* Collapsed sidebar: centre icons, hide text */ -.sidebar--collapsed .nav-group__label { - display: none; -} - -.sidebar--collapsed .nav-group { - gap: 4px; - margin-bottom: 0; -} - -/* In collapsed sidebar, always show nav items (icon-only) regardless of group collapse state */ -.sidebar--collapsed .nav-group--collapsed .nav-group__items { - display: grid; -} - -.sidebar--collapsed .nav-item { - justify-content: center; - width: 44px; - height: 42px; - padding: 0; - margin: 0 auto; - border-radius: 16px; -} - -.sidebar--collapsed .nav-item__icon { - width: 18px; - height: 18px; - opacity: 0.78; -} - -.sidebar--collapsed .nav-item__icon svg { - width: 18px; - height: 18px; -} - -.sidebar--collapsed .nav-item__text { - display: none; -} - -.sidebar--collapsed .nav-item__external-icon { - display: none; -} - -/* Footer: docs link + version */ -.sidebar-footer { - flex-shrink: 0; - padding: 8px; - border-top: 1px solid var(--border); -} - -.sidebar--collapsed .sidebar-footer { - padding: 12px 8px 10px; -} - -.sidebar-footer__docs-block { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -.sidebar--collapsed .sidebar-footer__docs-block { - align-items: center; - gap: 10px; -} - -.sidebar--collapsed .sidebar-footer .nav-item { - justify-content: center; - width: 44px; - height: 44px; - padding: 0; -} - -.sidebar-version { - display: flex; - align-items: center; - justify-content: center; - padding: 4px 10px; -} - -.sidebar-version__text { - font-size: 11px; - color: var(--muted); - font-weight: 500; - letter-spacing: 0.02em; -} - -.sidebar-version__dot { - width: 8px; - height: 8px; - border-radius: var(--radius-full); - background: color-mix(in srgb, var(--accent) 78%, white 22%); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent); - opacity: 1; - margin: 0 auto; -} - -/* Drag-to-resize handle */ -.sidebar-resizer { - width: 3px; - cursor: col-resize; - flex-shrink: 0; - background: transparent; - transition: background var(--duration-fast) ease; - position: relative; -} - -.sidebar-resizer::after { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 3px; - background: transparent; - transition: background var(--duration-fast) ease; -} - -.sidebar-resizer:hover::after { - background: var(--accent); - opacity: 0.35; -} - -.sidebar-resizer:active::after { - background: var(--accent); - opacity: 0.6; -} - -/* Shell-level collapsed / focus overrides */ -.shell--nav-collapsed .shell-nav { - width: var(--shell-nav-rail-width); - min-width: var(--shell-nav-rail-width); -} - -.shell--chat-focus .shell-nav { - width: 0; - min-width: 0; - overflow: hidden; - pointer-events: none; - opacity: 0; -} - -/* Nav collapse toggle */ .nav-collapse-toggle { - width: 28px; - height: 28px; + width: 36px; + height: 36px; display: flex; align-items: center; justify-content: center; - background: transparent; - border: 1px solid transparent; - border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--bg-elevated) 88%, transparent); + border: 1px solid color-mix(in srgb, var(--border-strong) 68%, transparent); + border-radius: 999px; cursor: pointer; transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, - color var(--duration-fast) ease; + color var(--duration-fast) ease, + transform var(--duration-fast) ease; margin-bottom: 0; color: var(--muted); + box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); } .nav-collapse-toggle:hover { - background: var(--bg-hover); + background: color-mix(in srgb, var(--bg-hover) 90%, transparent); + border-color: color-mix(in srgb, var(--border-strong) 88%, transparent); color: var(--text); + transform: translateY(-1px); } .nav-collapse-toggle__icon { @@ -572,81 +478,65 @@ stroke-linejoin: round; } -.nav-collapse-toggle:hover .nav-collapse-toggle__icon { - color: inherit; -} - -/* Nav groups */ -.nav-group { - margin-bottom: 12px; +.nav-section { display: grid; - gap: 1px; + gap: 6px; + margin-bottom: 16px; } -.nav-group:last-child { +.nav-section:last-child { margin-bottom: 0; } -.nav-group__items { +.nav-section__items { display: grid; - gap: 1px; + gap: 4px; } -.nav-group--collapsed .nav-group__items { +.nav-section--collapsed .nav-section__items { display: none; } -.nav-group__label { +.nav-section__label { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; - padding: 5px 10px; - font-size: 10px; - font-weight: 600; - color: var(--muted); - margin-bottom: 2px; + padding: 0 12px; + min-height: 28px; background: transparent; border: none; + border-radius: 10px; + color: var(--muted); cursor: pointer; text-align: left; - text-transform: uppercase; - letter-spacing: 0.06em; - border-radius: var(--radius-sm); transition: color var(--duration-fast) ease, background var(--duration-fast) ease; } -.nav-group__label:hover { +.nav-section__label:hover { color: var(--text); - background: var(--bg-hover); + background: color-mix(in srgb, var(--bg-hover) 72%, transparent); } -.nav-group__label--static { - cursor: default; +.nav-section__label-text { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; } -.nav-group__label--static:hover { - color: var(--muted); - background: transparent; -} - -.nav-group__label-text { - flex: 1; -} - -.nav-group__chevron { +.nav-section__chevron { display: inline-flex; align-items: center; justify-content: center; - font-size: 10px; opacity: 0.5; transition: transform var(--duration-fast) ease; } -.nav-group__chevron svg { +.nav-section__chevron svg { width: 12px; height: 12px; stroke: currentColor; @@ -656,19 +546,19 @@ stroke-linejoin: round; } -.nav-group--collapsed .nav-group__chevron { +.nav-section--collapsed .nav-section__chevron { transform: rotate(-90deg); } -/* Nav items */ .nav-item { position: relative; display: flex; align-items: center; justify-content: flex-start; - gap: 8px; - padding: 7px 10px; - border-radius: var(--radius-md); + gap: 10px; + min-height: 38px; + padding: 0 12px; + border-radius: 12px; border: 1px solid transparent; background: transparent; color: var(--muted); @@ -677,23 +567,26 @@ transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease, - color var(--duration-fast) ease; + color var(--duration-fast) ease, + transform var(--duration-fast) ease; } .nav-item__icon { - width: 15px; - height: 15px; + width: 16px; + height: 16px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; - opacity: 0.6; - transition: opacity var(--duration-fast) ease; + opacity: 0.72; + transition: + opacity var(--duration-fast) ease, + color var(--duration-fast) ease; } .nav-item__icon svg { - width: 15px; - height: 15px; + width: 16px; + height: 16px; stroke: currentColor; fill: none; stroke-width: 1.5px; @@ -703,25 +596,29 @@ .nav-item__text { font-size: 13px; - font-weight: 450; + font-weight: 550; white-space: nowrap; } .nav-item:hover { color: var(--text); - background: var(--bg-hover); + background: color-mix(in srgb, var(--bg-hover) 84%, transparent); + border-color: color-mix(in srgb, var(--border) 72%, transparent); text-decoration: none; } .nav-item:hover .nav-item__icon { - opacity: 0.9; + opacity: 1; } .nav-item.active, .nav-item--active { color: var(--text-strong); - background: var(--accent-subtle); - border-color: color-mix(in srgb, var(--accent) 15%, transparent); + background: color-mix(in srgb, var(--accent-subtle) 88%, var(--bg-elevated) 12%); + border-color: color-mix(in srgb, var(--accent) 18%, transparent); + box-shadow: + inset 0 1px 0 color-mix(in srgb, white 10%, transparent), + 0 12px 24px color-mix(in srgb, black 10%, transparent); } .nav-item.active .nav-item__icon, @@ -730,40 +627,171 @@ color: var(--accent); } +.sidebar--collapsed .sidebar-shell { + padding: 12px 8px 10px; +} + +.sidebar--collapsed .sidebar-shell__header { + justify-content: center; + align-items: center; + gap: 0; + padding: 0 2px 16px; +} + +.sidebar--collapsed .sidebar-nav { + padding: 0; +} + +.sidebar--collapsed .nav-section { + gap: 6px; + margin-bottom: 16px; +} + +.sidebar--collapsed .nav-item { + justify-content: center; + width: 44px; + min-height: 44px; + padding: 0; + margin: 0 auto; + border-radius: 16px; + border-color: transparent; + box-shadow: none; +} + +.sidebar--collapsed .nav-item__icon { + width: 18px; + height: 18px; +} + +.sidebar--collapsed .nav-item__icon svg { + width: 18px; + height: 18px; +} + +.sidebar--collapsed .nav-item__text, +.sidebar--collapsed .nav-item__external-icon { + display: none; +} + .sidebar--collapsed .nav-item--active::before, .sidebar--collapsed .nav-item.active::before { content: ""; position: absolute; - left: 6px; - top: 11px; - bottom: 11px; - width: 2px; + left: 8px; + top: 10px; + bottom: 10px; + width: 3px; border-radius: 999px; - background: color-mix(in srgb, var(--accent) 78%, transparent); + background: color-mix(in srgb, #2de3d1 86%, transparent); + box-shadow: 0 0 14px color-mix(in srgb, #2de3d1 34%, transparent); } .sidebar--collapsed .nav-item.active, .sidebar--collapsed .nav-item--active { - background: color-mix(in srgb, var(--accent-subtle) 88%, var(--bg-elevated) 12%); - border-color: color-mix(in srgb, var(--accent) 12%, var(--border) 88%); - box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, #0b2f34 84%, var(--bg-elevated) 16%) 0%, + color-mix(in srgb, #081f25 90%, var(--bg) 10%) 100% + ); + border-color: color-mix(in srgb, #1ed2c2 18%, var(--border) 82%); + box-shadow: + inset 0 1px 0 color-mix(in srgb, white 8%, transparent), + 0 10px 20px color-mix(in srgb, black 18%, transparent); } .sidebar--collapsed .nav-collapse-toggle { - width: 44px; - height: 34px; - margin-bottom: 0; - border-color: color-mix(in srgb, var(--border-strong) 74%, transparent); - border-radius: var(--radius-full); + width: 42px; + height: 42px; + border-color: color-mix(in srgb, var(--border) 82%, transparent); background: color-mix(in srgb, var(--bg-elevated) 92%, transparent); box-shadow: - inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent), + inset 0 1px 0 color-mix(in srgb, white 8%, transparent), 0 8px 18px color-mix(in srgb, black 16%, transparent); } -.sidebar--collapsed .nav-collapse-toggle:hover { - border-color: color-mix(in srgb, var(--border-strong) 72%, transparent); - background: color-mix(in srgb, var(--bg-elevated) 96%, transparent); +.sidebar--collapsed .sidebar-brand__logo { + width: 34px; + height: 34px; + border-radius: 12px; + box-shadow: + 0 10px 20px color-mix(in srgb, black 20%, transparent), + inset 0 1px 0 color-mix(in srgb, white 10%, transparent); +} + +.sidebar-utility-group { + display: grid; + gap: 8px; +} + +.sidebar-utility-link { + min-height: 42px; +} + +.sidebar-version { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-height: 40px; + padding: 0 12px; + border-radius: 14px; + background: color-mix(in srgb, var(--bg-elevated) 72%, transparent); + border: 1px solid color-mix(in srgb, var(--border) 72%, transparent); +} + +.sidebar-version__label { + font-size: 11px; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.sidebar-version__text { + font-size: 12px; + color: var(--text); + font-weight: 600; +} + +.sidebar-version__dot { + width: 8px; + height: 8px; + border-radius: var(--radius-full); + background: color-mix(in srgb, var(--accent) 78%, white 22%); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent); + opacity: 1; + margin: 0 auto; +} + +.sidebar--collapsed .sidebar-shell__footer { + padding: 8px 0 2px; +} + +.sidebar--collapsed .sidebar-utility-group { + justify-items: center; + gap: 6px; +} + +.sidebar--collapsed .sidebar-version { + width: 44px; + min-height: 44px; + padding: 0; + justify-content: center; + border-radius: 16px; +} + +.shell--nav-collapsed .shell-nav { + width: var(--shell-nav-rail-width); + min-width: var(--shell-nav-rail-width); +} + +.shell--chat-focus .shell-nav { + width: 0; + min-width: 0; + overflow: hidden; + pointer-events: none; + opacity: 0; + border-right-width: 0; } .nav-item__external-icon { @@ -955,12 +983,6 @@ "content"; } - .nav-group { - grid-auto-flow: column; - grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); - margin-bottom: 0; - } - .grid-cols-2, .grid-cols-3 { grid-template-columns: 1fr; diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css index b871fe1d440..3c929435a7b 100644 --- a/ui/src/styles/layout.mobile.css +++ b/ui/src/styles/layout.mobile.css @@ -2,61 +2,131 @@ Mobile Layout =========================================== */ -/* Tablet and smaller: collapse the left nav into a horizontal rail. */ +/* Tablet and smaller: switch the left nav to a slide-over drawer. */ @media (max-width: 1100px) { .shell, .shell--nav-collapsed { grid-template-columns: minmax(0, 1fr); - grid-template-rows: var(--shell-topbar-height) auto minmax(0, 1fr); + grid-template-rows: var(--shell-topbar-height) minmax(0, 1fr); grid-template-areas: "topbar" - "nav" "content"; } .shell--chat-focus { - grid-template-rows: var(--shell-topbar-height) 0 minmax(0, 1fr); + grid-template-rows: var(--shell-topbar-height) minmax(0, 1fr); } .shell-nav, .shell--nav-collapsed .shell-nav { - width: auto; + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 70; + width: min(86vw, 320px); min-width: 0; - border-bottom: 1px solid var(--border); + border-right: none; + box-shadow: 0 30px 80px color-mix(in srgb, black 40%, transparent); + transform: translateX(-100%); + opacity: 0; + pointer-events: none; + transition: + transform var(--shell-focus-duration) var(--shell-focus-ease), + opacity var(--shell-focus-duration) var(--shell-focus-ease); + } + + .shell--nav-collapsed:not(.shell--nav-drawer-open) .shell-nav { + width: var(--shell-nav-rail-width); + transform: translateX(0); + opacity: 1; + pointer-events: auto; + box-shadow: none; + } + + .shell--nav-drawer-open .shell-nav, + .shell--nav-collapsed.shell--nav-drawer-open .shell-nav { + transform: translateX(0); + opacity: 1; + pointer-events: auto; + } + + .shell-nav-backdrop { + display: block; + position: fixed; + inset: 0; + z-index: 65; + border: 0; + background: color-mix(in srgb, black 52%, transparent); + opacity: 0; + pointer-events: none; + transition: opacity var(--shell-focus-duration) var(--shell-focus-ease); + } + + .shell--nav-drawer-open .shell-nav-backdrop { + opacity: 1; + pointer-events: auto; + } + + /* Show the hamburger toggle at the same breakpoint where the drawer takes over. */ + .topbar-nav-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + padding: 0; + border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-elevated) 80%, transparent); + color: var(--muted); + box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); } .sidebar, .sidebar--collapsed { - width: auto; + width: 100%; min-width: 0; flex: 1 1 auto; - flex-direction: row; - align-items: center; + flex-direction: column; + align-items: stretch; border-right: none; } - .sidebar-header, - .sidebar--collapsed .sidebar-header { - justify-content: flex-start; - padding: 8px 10px; - flex: 0 0 auto; + .sidebar-shell, + .sidebar--collapsed .sidebar-shell { + padding: 18px 16px 14px; + border-radius: 0; } - .sidebar-brand { + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar-shell, + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .sidebar-shell { + padding: 12px 8px 10px; + } + + .sidebar-shell__header { + min-height: 0; + padding: 0 4px 16px; + } + + .sidebar-shell__header .nav-collapse-toggle { display: none; } + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar-shell__header { + justify-content: center; + align-items: center; + gap: 0; + padding: 0 2px 16px; + } + .sidebar-nav, .sidebar--collapsed .sidebar-nav { flex: 1 1 auto; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - gap: 8px; - padding: 8px 10px 8px 0; - overflow-x: auto; - overflow-y: hidden; - -webkit-overflow-scrolling: touch; + display: block; + padding: 0; + overflow-x: hidden; + overflow-y: auto; scrollbar-width: none; } @@ -65,29 +135,36 @@ display: none; } - .nav-group, - .nav-group__items, - .sidebar--collapsed .nav-group, - .sidebar--collapsed .nav-group__items { - display: contents; + .nav-section, + .sidebar--collapsed .nav-section { + display: grid; + margin-bottom: 16px; } - .nav-group { - margin-bottom: 0; - } - - .sidebar-nav .nav-group__label { - display: none; + .sidebar-nav .nav-section__label, + .sidebar--collapsed .nav-section__label { + display: flex; } .nav-item, .sidebar--collapsed .nav-item { margin: 0; - padding: 8px 14px; + min-height: 40px; + padding: 0 12px; font-size: 13px; - border-radius: var(--radius-md); + border-radius: 12px; white-space: nowrap; flex: 0 0 auto; + width: auto; + } + + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .nav-item { + justify-content: center; + width: 44px; + min-height: 44px; + padding: 0; + margin: 0 auto; + border-radius: 16px; } .sidebar--collapsed .nav-item--active::before, @@ -95,14 +172,53 @@ content: none; } - .sidebar-footer, - .sidebar--collapsed .sidebar-footer { + .sidebar--collapsed .nav-item__text, + .sidebar--collapsed .nav-item__external-icon { + display: inline-flex; + } + + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .nav-item__text, + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .nav-item__external-icon { display: none; } + + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .nav-item--active::before, + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .nav-item.active::before { + content: ""; + position: absolute; + left: 8px; + top: 10px; + bottom: 10px; + width: 3px; + border-radius: 999px; + background: color-mix(in srgb, #2de3d1 86%, transparent); + box-shadow: 0 0 14px color-mix(in srgb, #2de3d1 34%, transparent); + } + + .sidebar--collapsed .sidebar-shell__footer { + padding: 12px 8px 0; + } + + .sidebar--collapsed .sidebar-version { + width: auto; + min-height: 40px; + padding: 0 12px; + } + + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .sidebar-shell__footer { + padding: 8px 0 2px; + } + + .shell--nav-collapsed:not(.shell--nav-drawer-open) .sidebar--collapsed .sidebar-version { + width: 44px; + min-height: 44px; + padding: 0; + justify-content: center; + } } /* Mobile-specific styles */ -@media (max-width: 600px) { +@media (max-width: 768px) { .shell { --shell-pad: 8px; --shell-gap: 8px; @@ -111,24 +227,40 @@ /* Topbar */ .topbar { padding: 10px 12px; - gap: 8px; - flex-direction: row; + min-height: auto; + } + + .topnav-shell { flex-wrap: wrap; - justify-content: space-between; - align-items: center; + gap: 10px; } - .brand { - flex: 1; + .topnav-shell__actions { min-width: 0; + flex: 1 1 auto; + justify-content: space-between; + gap: 10px; + align-items: stretch; } - .brand-title { - font-size: 14px; + .topnav-shell__content { + order: 3; + width: 100%; } - .brand-sub { - display: none; + .topbar-nav-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 38px; + height: 38px; + padding: 0; + border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-elevated) 80%, transparent); + color: var(--muted); + box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); } .topbar-status { @@ -137,6 +269,15 @@ flex-wrap: nowrap; } + .topbar-search { + min-width: 0; + flex: 1; + } + + .topbar-theme-mode { + flex-shrink: 0; + } + .topbar-status .pill { padding: 4px 8px; font-size: 11px; @@ -151,25 +292,23 @@ display: none; } - .shell-nav { - border-bottom-width: 0; + .shell-nav, + .shell--nav-collapsed .shell-nav { + width: min(92vw, 320px); } - .sidebar-header { - padding: 6px 8px; + .shell--nav-collapsed:not(.shell--nav-drawer-open) .shell-nav { + width: 78px; } - .sidebar-nav { - gap: 6px; - padding: 6px 8px 6px 0; + .sidebar-shell, + .sidebar--collapsed .sidebar-shell { + padding: 16px 14px 12px; } - .nav-item { - padding: 6px 10px; + .nav-item, + .sidebar--collapsed .nav-item { font-size: 12px; - border-radius: var(--radius-md); - white-space: nowrap; - flex-shrink: 0; } /* Content */ @@ -177,6 +316,19 @@ display: none; } + .content--chat .content-header { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + } + + .content--chat .content-header > div:first-child, + .content--chat .page-meta, + .content--chat .chat-controls { + width: 100%; + } + .content { padding: 4px 4px 16px; gap: 12px; diff --git a/ui/src/ui/app-chat.test.ts b/ui/src/ui/app-chat.test.ts index 1fcdf14db7f..9a3e86d375d 100644 --- a/ui/src/ui/app-chat.test.ts +++ b/ui/src/ui/app-chat.test.ts @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ import { afterEach, describe, expect, it, vi } from "vitest"; -import { refreshChatAvatar, type ChatHost } from "./app-chat.ts"; +import { handleSendChat, refreshChatAvatar, type ChatHost } from "./app-chat.ts"; function makeHost(overrides?: Partial): ChatHost { return { @@ -19,7 +19,11 @@ function makeHost(overrides?: Partial): ChatHost { basePath: "", hello: null, chatAvatarUrl: null, + chatModelOverrides: {}, + chatModelsLoading: false, + chatModelCatalog: [], refreshSessionsAfterChat: new Set(), + updateComplete: Promise.resolve(), ...overrides, }; } @@ -63,3 +67,55 @@ describe("refreshChatAvatar", () => { expect(host.chatAvatarUrl).toBeNull(); }); }); + +describe("handleSendChat", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("keeps slash-command model changes in sync with the chat header cache", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + json: async () => ({}), + }) as unknown as typeof fetch, + ); + const request = vi.fn(async (method: string, _params?: unknown) => { + if (method === "sessions.patch") { + return { ok: true, key: "main" }; + } + if (method === "chat.history") { + return { messages: [], thinkingLevel: null }; + } + if (method === "sessions.list") { + return { + ts: 0, + path: "", + count: 0, + defaults: { model: "gpt-5", contextTokens: null }, + sessions: [], + }; + } + if (method === "models.list") { + return { + models: [{ id: "gpt-5-mini", name: "GPT-5 Mini", provider: "openai" }], + }; + } + throw new Error(`Unexpected request: ${method}`); + }); + const host = makeHost({ + client: { request } as unknown as ChatHost["client"], + sessionKey: "main", + chatMessage: "/model gpt-5-mini", + }); + + await handleSendChat(host); + + expect(request).toHaveBeenCalledWith("sessions.patch", { + key: "main", + model: "gpt-5-mini", + }); + expect(host.chatModelOverrides.main).toBe("gpt-5-mini"); + }); +}); diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index 05f6aa8c9e2..c877b4c5a5d 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -6,9 +6,11 @@ import type { OpenClawApp } from "./app.ts"; import { executeSlashCommand } from "./chat/slash-command-executor.ts"; import { parseSlashCommand } from "./chat/slash-commands.ts"; import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat.ts"; +import { loadModels } from "./controllers/models.ts"; import { loadSessions } from "./controllers/sessions.ts"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts"; import { normalizeBasePath } from "./navigation.ts"; +import type { ModelCatalogEntry } from "./types.ts"; import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts"; import { generateUUID } from "./uuid.ts"; @@ -27,6 +29,10 @@ export type ChatHost = { basePath: string; hello: GatewayHelloOk | null; chatAvatarUrl: string | null; + chatModelOverrides: Record; + chatModelsLoading: boolean; + chatModelCatalog: ModelCatalogEntry[]; + updateComplete?: Promise; refreshSessionsAfterChat: Set; /** Callback for slash-command side effects that need app-level access. */ onSlashAction?: (action: string) => void; @@ -295,12 +301,20 @@ async function dispatchSlashCommand( return; } - const result = await executeSlashCommand(host.client, host.sessionKey, name, args); + const targetSessionKey = host.sessionKey; + const result = await executeSlashCommand(host.client, targetSessionKey, name, args); if (result.content) { injectCommandResult(host, result.content); } + if (result.sessionPatch && "model" in result.sessionPatch) { + host.chatModelOverrides = { + ...host.chatModelOverrides, + [targetSessionKey]: result.sessionPatch.model ?? null, + }; + } + if (result.action === "refresh") { await refreshChat(host); } @@ -341,16 +355,31 @@ export async function refreshChat(host: ChatHost, opts?: { scheduleScroll?: bool loadSessions(host as unknown as OpenClawApp, { activeMinutes: 0, limit: 0, - includeGlobal: false, - includeUnknown: false, + includeGlobal: true, + includeUnknown: true, }), refreshChatAvatar(host), + refreshChatModels(host), ]); if (opts?.scheduleScroll !== false) { scheduleChatScroll(host as unknown as Parameters[0]); } } +async function refreshChatModels(host: ChatHost) { + if (!host.client || !host.connected) { + host.chatModelsLoading = false; + host.chatModelCatalog = []; + return; + } + host.chatModelsLoading = true; + try { + host.chatModelCatalog = await loadModels(host.client); + } finally { + host.chatModelsLoading = false; + } +} + export const flushChatQueueForEvent = flushChatQueue; type SessionDefaultsSnapshot = { diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 0a2003fac34..0ebafc22d4d 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -12,7 +12,7 @@ import { icons } from "./icons.ts"; import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts"; import type { ThemeTransitionContext } from "./theme-transition.ts"; import type { ThemeMode, ThemeName } from "./theme.ts"; -import type { SessionsListResult } from "./types.ts"; +import type { ModelCatalogEntry, SessionsListResult } from "./types.ts"; type SessionDefaultsSnapshot = { mainSessionKey?: string; @@ -49,10 +49,10 @@ function resetChatStateForSessionSwitch(state: AppViewState, sessionKey: string) }); } -export function renderTab(state: AppViewState, tab: Tab) { +export function renderTab(state: AppViewState, tab: Tab, opts?: { collapsed?: boolean }) { const href = pathForTab(tab, state.basePath); const isActive = state.tab === tab; - const collapsed = state.settings.navCollapsed; + const collapsed = opts?.collapsed ?? state.settings.navCollapsed; return html` + ${modelSelect} `; } @@ -316,11 +318,139 @@ async function refreshSessionOptions(state: AppViewState) { await loadSessions(state as unknown as Parameters[0], { activeMinutes: 0, limit: 0, - includeGlobal: false, - includeUnknown: false, + includeGlobal: true, + includeUnknown: true, }); } +function resolveActiveSessionRow(state: AppViewState) { + return state.sessionsResult?.sessions?.find((row) => row.key === state.sessionKey); +} + +function resolveModelOverrideValue(state: AppViewState): string { + // Prefer the local cache — it reflects in-flight patches before sessionsResult refreshes. + const cached = state.chatModelOverrides[state.sessionKey]; + if (typeof cached === "string") { + return cached.trim(); + } + // cached === null means explicitly cleared to default. + if (cached === null) { + return ""; + } + // No local override recorded yet — fall back to server data. + const activeRow = resolveActiveSessionRow(state); + if (activeRow) { + return typeof activeRow.model === "string" ? activeRow.model.trim() : ""; + } + return ""; +} + +function resolveDefaultModelValue(state: AppViewState): string { + const model = state.sessionsResult?.defaults?.model; + return typeof model === "string" ? model.trim() : ""; +} + +function buildChatModelOptions( + catalog: ModelCatalogEntry[], + currentOverride: string, + defaultModel: string, +): Array<{ value: string; label: string }> { + const seen = new Set(); + const options: Array<{ value: string; label: string }> = []; + const addOption = (value: string, label?: string) => { + const trimmed = value.trim(); + if (!trimmed) { + return; + } + const key = trimmed.toLowerCase(); + if (seen.has(key)) { + return; + } + seen.add(key); + options.push({ value: trimmed, label: label ?? trimmed }); + }; + + for (const entry of catalog) { + const provider = entry.provider?.trim(); + addOption(entry.id, provider ? `${entry.id} · ${provider}` : entry.id); + } + + if (currentOverride) { + addOption(currentOverride); + } + if (defaultModel) { + addOption(defaultModel); + } + return options; +} + +function renderChatModelSelect(state: AppViewState) { + const currentOverride = resolveModelOverrideValue(state); + const defaultModel = resolveDefaultModelValue(state); + const options = buildChatModelOptions( + state.chatModelCatalog ?? [], + currentOverride, + defaultModel, + ); + const defaultLabel = defaultModel ? `Default (${defaultModel})` : "Default model"; + const busy = + state.chatLoading || state.chatSending || Boolean(state.chatRunId) || state.chatStream !== null; + const disabled = + !state.connected || busy || (state.chatModelsLoading && options.length === 0) || !state.client; + return html` + + `; +} + +async function switchChatModel(state: AppViewState, nextModel: string) { + if (!state.client || !state.connected) { + return; + } + const currentOverride = resolveModelOverrideValue(state); + if (currentOverride === nextModel) { + return; + } + const targetSessionKey = state.sessionKey; + const prevOverride = state.chatModelOverrides[targetSessionKey]; + state.lastError = null; + // Write the override cache immediately so the picker stays in sync during the RPC round-trip. + state.chatModelOverrides = { + ...state.chatModelOverrides, + [targetSessionKey]: nextModel || null, + }; + try { + await state.client.request("sessions.patch", { + key: targetSessionKey, + model: nextModel || null, + }); + await refreshSessionOptions(state); + } catch (err) { + // Roll back so the picker reflects the actual server model. + state.chatModelOverrides = { ...state.chatModelOverrides, [targetSessionKey]: prevOverride }; + state.lastError = `Failed to set model: ${String(err)}`; + } +} + /* ── Channel display labels ────────────────────────────── */ const CHANNEL_LABELS: Record = { bluebubbles: "iMessage", @@ -504,6 +634,9 @@ export function resolveSessionOptionGroups( }; for (const row of rows) { + if (row.key !== sessionKey && (row.kind === "global" || row.kind === "unknown")) { + continue; + } if (hideCron && row.key !== sessionKey && isCronSessionKey(row.key)) { continue; } diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 74644f07708..b1ddf9e323c 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -264,33 +264,6 @@ type AutomationSectionKey = (typeof AUTOMATION_SECTION_KEYS)[number]; type InfrastructureSectionKey = (typeof INFRASTRUCTURE_SECTION_KEYS)[number]; type AiAgentsSectionKey = (typeof AI_AGENTS_SECTION_KEYS)[number]; -const NAV_WIDTH_MIN = 200; -const NAV_WIDTH_MAX = 400; - -function handleNavResizeStart(e: MouseEvent, state: AppViewState) { - e.preventDefault(); - const startX = e.clientX; - const startWidth = state.settings.navWidth; - - const onMove = (ev: MouseEvent) => { - const delta = ev.clientX - startX; - const next = Math.round(Math.min(NAV_WIDTH_MAX, Math.max(NAV_WIDTH_MIN, startWidth + delta))); - state.applySettings({ ...state.settings, navWidth: next }); - }; - - const onUp = () => { - document.removeEventListener("mousemove", onMove); - document.removeEventListener("mouseup", onUp); - document.body.style.cursor = ""; - document.body.style.userSelect = ""; - }; - - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - document.addEventListener("mousemove", onMove); - document.addEventListener("mouseup", onUp); -} - function resolveAssistantAvatarUrl(state: AppViewState): string | undefined { const list = state.agentsList?.agents ?? []; const parsed = parseAgentSessionKey(state.sessionKey); @@ -330,6 +303,8 @@ export function renderApp(state: AppViewState) { const chatDisabledReason = state.connected ? null : t("chat.disconnected"); const isChat = state.tab === "chat"; const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding); + const navDrawerOpen = Boolean(state.navDrawerOpen && !chatFocus && !state.onboarding); + const navCollapsed = Boolean(state.settings.navCollapsed && !navDrawerOpen); const showThinking = state.onboarding ? false : state.settings.chatShowThinking; const assistantAvatarUrl = resolveAssistantAvatarUrl(state); const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null; @@ -423,144 +398,164 @@ export function renderApp(state: AppViewState) { }, })}
+
- - -
- ${renderTopbarThemeModeToggle(state)} +
+ +
+ +
+
+ +
+ ${renderTopbarThemeModeToggle(state)} +
+
-
+
+
- - - ${ - !state.settings.navCollapsed && !chatFocus - ? html` - - ` - : nothing - } +
${ diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index b659c195754..ad2910625b6 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -71,11 +71,15 @@ export type AppViewState = { fallbackStatus: FallbackStatus | null; chatAvatarUrl: string | null; chatThinkingLevel: string | null; + chatModelOverrides: Record; + chatModelsLoading: boolean; + chatModelCatalog: ModelCatalogEntry[]; chatQueue: ChatQueueItem[]; chatManualRefreshInFlight: boolean; nodesLoading: boolean; nodes: Array>; chatNewMessagesBelow: boolean; + navDrawerOpen: boolean; sidebarOpen: boolean; sidebarContent: string | null; sidebarError: string | null; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 7f936722ca5..1b3971a41f6 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -158,9 +158,13 @@ export class OpenClawApp extends LitElement { @state() fallbackStatus: FallbackStatus | null = null; @state() chatAvatarUrl: string | null = null; @state() chatThinkingLevel: string | null = null; + @state() chatModelOverrides: Record = {}; + @state() chatModelsLoading = false; + @state() chatModelCatalog: ModelCatalogEntry[] = []; @state() chatQueue: ChatQueueItem[] = []; @state() chatAttachments: ChatAttachment[] = []; @state() chatManualRefreshInFlight = false; + @state() navDrawerOpen = false; onSlashAction?: (action: string) => void; @@ -541,6 +545,7 @@ export class OpenClawApp extends LitElement { setTab(next: Tab) { setTabInternal(this as unknown as Parameters[0], next); + this.navDrawerOpen = false; } setTheme(next: ThemeName, context?: Parameters[2]) { diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 9a7f7d2eeb2..6b584be512b 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -174,7 +174,11 @@ export function renderMessageGroup( ${timestamp} ${renderMessageMeta(meta)} ${normalizedRole === "assistant" && isTtsSupported() ? renderTtsButton(group) : nothing} - ${opts.onDelete ? renderDeleteButton(opts.onDelete) : nothing} + ${ + opts.onDelete + ? renderDeleteButton(opts.onDelete, normalizedRole === "user" ? "left" : "right") + : nothing + } @@ -312,6 +316,8 @@ function extractGroupText(group: MessageGroup): string { const SKIP_DELETE_CONFIRM_KEY = "openclaw:skipDeleteConfirm"; +type DeleteConfirmSide = "left" | "right"; + function shouldSkipDeleteConfirm(): boolean { try { return localStorage.getItem(SKIP_DELETE_CONFIRM_KEY) === "1"; @@ -320,7 +326,7 @@ function shouldSkipDeleteConfirm(): boolean { } } -function renderDeleteButton(onDelete: () => void) { +function renderDeleteButton(onDelete: () => void, side: DeleteConfirmSide) { return html` - ` - : nothing - } +
+ + + + + + props.onSearchChange((e.target as HTMLInputElement).value)} + /> + ${ + props.searchQuery + ? html` + + ` + : nothing + } +
` : nothing diff --git a/ui/src/ui/views/skills.ts b/ui/src/ui/views/skills.ts index ad0f4ee63c0..b9338971c8e 100644 --- a/ui/src/ui/views/skills.ts +++ b/ui/src/ui/views/skills.ts @@ -61,6 +61,8 @@ export function renderSkills(props: SkillsProps) { .value=${props.filter} @input=${(e: Event) => props.onFilterChange((e.target as HTMLInputElement).value)} placeholder="Search skills" + autocomplete="off" + name="skills-filter" />
${filtered.length} shown