diff --git a/CHANGELOG.md b/CHANGELOG.md index e730c7957..7376b5c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ - Sessions: support session `label` in store/list/UI and allow `sessions_send` lookup by label. (#570) — thanks @azade-c - Control UI: show/patch per-session reasoning level and render extracted reasoning in chat. - Control UI: queue outgoing chat messages, add Enter-to-send, and show queued items. (#527) — thanks @YuriNachos +- Control UI: refactor chat layout with tool sidebar, grouped messages, and nav improvements. (#475) — thanks @rahthakor - Control UI: drop explicit `ui:install` step; `ui:build` now auto-installs UI deps (docs + update flow). - Telegram: retry long-polling conflicts with backoff to avoid fatal exits. - Telegram: fix grammY fetch type mismatch when injecting `fetch`. (#512) — thanks @YuriNachos diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index 19a961bf9..6279f2be0 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { resetProcessRegistryForTests } from "./bash-process-registry.js"; import { bashTool, @@ -50,6 +50,16 @@ beforeEach(() => { }); describe("bash tool backgrounding", () => { + const originalShell = process.env.SHELL; + + beforeEach(() => { + if (!isWin) process.env.SHELL = "/bin/bash"; + }); + + afterEach(() => { + if (!isWin) process.env.SHELL = originalShell; + }); + it( "backgrounds after yield and can be polled", async () => { diff --git a/ui/index.html b/ui/index.html index 411354ea1..910cb59f8 100644 --- a/ui/index.html +++ b/ui/index.html @@ -12,4 +12,3 @@ - diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index bb86c202f..0b584d88f 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -94,29 +94,7 @@ body::before { z-index: 0; } -body::after { - content: ""; - position: fixed; - inset: 0; - background: - repeating-linear-gradient( - 90deg, - var(--grid-line) 0, - var(--grid-line) 1px, - transparent 1px, - transparent 140px - ), - repeating-linear-gradient( - 0deg, - var(--grid-line) 0, - var(--grid-line) 1px, - transparent 1px, - transparent 140px - ); - opacity: 0.45; - pointer-events: none; - z-index: 0; -} +/* Grid overlay removed for cleaner look */ @keyframes theme-circle-transition { 0% { diff --git a/ui/src/styles/chat.css b/ui/src/styles/chat.css new file mode 100644 index 000000000..694bd110d --- /dev/null +++ b/ui/src/styles/chat.css @@ -0,0 +1,789 @@ +/* ============================================= + CHAT CARD LAYOUT - Flex container with sticky compose + ============================================= */ + +/* Main chat card - flex column layout, transparent background */ +.chat { + position: relative; + display: flex; + flex-direction: column; + flex: 1 1 0; + height: 100%; + min-height: 0; /* Allow flex shrinking */ + overflow: hidden; + background: transparent !important; + border: none !important; + box-shadow: none !important; +} + +/* Chat header - fixed at top, transparent */ +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: nowrap; + flex-shrink: 0; + padding-bottom: 12px; + margin-bottom: 12px; + background: transparent; +} + +.chat-header__left { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + min-width: 0; +} + +.chat-header__right { + display: flex; + align-items: center; + gap: 8px; +} + +.chat-session { + min-width: 180px; +} + +/* Chat thread - scrollable middle section, transparent */ +.chat-thread { + flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */ + overflow-y: auto; + overflow-x: hidden; + padding: 12px; + margin: 0 -12px; + min-height: 0; /* Allow shrinking for flex scroll behavior */ + border-radius: 12px; + background: transparent; +} + +/* Focus mode exit button */ +.chat-focus-exit { + position: absolute; + top: 12px; + right: 12px; + z-index: 100; + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid var(--border); + background: var(--panel); + color: var(--muted); + font-size: 20px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + 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); +} + +.chat-focus-exit:hover { + background: var(--panel-strong); + color: var(--text); + border-color: var(--accent); +} + +/* Chat compose - sticky at bottom */ +.chat-compose { + position: sticky; + bottom: 0; + flex-shrink: 0; + display: flex; + align-items: flex-end; + gap: 12px; + margin-top: auto; /* Push to bottom of flex container */ + padding: 16px 0 4px; + background: linear-gradient(to bottom, transparent, var(--bg) 20%); + z-index: 10; +} + +.chat-compose__field { + flex: 1 1 auto; + min-width: 0; +} + +/* Hide the "Message" label - keep textarea only */ +.chat-compose__field > span { + display: none; +} + +/* Override .field textarea min-height (180px) from components.css */ +.chat-compose .chat-compose__field textarea { + width: 100%; + min-height: 36px; + max-height: 150px; + padding: 8px 12px; + border-radius: 10px; + resize: vertical; + white-space: pre-wrap; + font-family: var(--font-body); + font-size: 14px; + line-height: 1.45; +} + +.chat-compose__field textarea:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.chat-compose__actions { + flex-shrink: 0; + display: flex; + align-items: stretch; +} + +.chat-compose .chat-compose__actions .btn { + padding: 8px 16px; + font-size: 13px; + min-height: 36px; + white-space: nowrap; +} + +/* Chat controls - moved to content-header area, left aligned */ +.chat-controls { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + flex-wrap: wrap; +} + +.chat-controls__session { + min-width: 140px; +} + +.chat-controls__thinking { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; +} + +/* Icon button style */ +.btn--icon { + padding: 8px !important; + min-width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.06); +} + +/* Controls separator */ +.chat-controls__separator { + color: rgba(255, 255, 255, 0.4); + font-size: 18px; + margin: 0 8px; + font-weight: 300; +} + +:root[data-theme="light"] .chat-controls__separator { + color: rgba(16, 24, 40, 0.3); +} + +.btn--icon:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.2); +} + +/* Light theme icon button overrides */ +:root[data-theme="light"] .btn--icon { + 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); + color: rgba(16, 24, 40, 0.7); +} + +:root[data-theme="light"] .btn--icon:hover { + background: rgba(255, 255, 255, 1); + border-color: rgba(16, 24, 40, 0.3); + color: rgba(16, 24, 40, 0.9); +} + +.btn--icon svg { + display: block; +} + +.chat-controls__session select { + padding: 6px 10px; + font-size: 13px; +} + +.chat-controls__thinking { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + padding: 4px 10px; + background: rgba(255, 255, 255, 0.04); + border-radius: 6px; + border: 1px solid var(--border); +} + +/* Light theme thinking indicator override */ +:root[data-theme="light"] .chat-controls__thinking { + background: rgba(255, 255, 255, 0.9); + border-color: rgba(16, 24, 40, 0.15); +} + +@media (max-width: 640px) { + .chat-session { + min-width: 140px; + } + + .chat-compose { + grid-template-columns: 1fr; + } + + .chat-controls { + flex-wrap: wrap; + gap: 8px; + } + + .chat-controls__session { + min-width: 120px; + } +} + +/* ============================================= + LEGACY CHAT LINE LAYOUT (non-grouped) + ============================================= */ + +.chat-line { + display: flex; + margin-bottom: 12px; +} + +.chat-line.user { + justify-content: flex-end; +} + +.chat-line.assistant, +.chat-line.other { + justify-content: flex-start; +} + +.chat-msg { + display: grid; + gap: 6px; + max-width: min(900px, 95%); +} + +.chat-line.user .chat-msg { + justify-items: end; +} + +.chat-stamp { + font-size: 11px; + color: var(--muted); +} + +.chat-line.user .chat-stamp { + text-align: right; +} + +/* ============================================= + CHAT TEXT STYLING + ============================================= */ + +.chat-text { + font-size: 14px; + line-height: 1.5; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.chat-text :where(p, ul, ol, pre, blockquote, table) { + margin: 0; +} + +.chat-text :where(p + p, p + ul, p + ol, p + pre, p + blockquote) { + margin-top: 0.75em; +} + +.chat-text :where(ul, ol) { + padding-left: 1.5em; +} + +.chat-text :where(li + li) { + margin-top: 0.25em; +} + +.chat-text :where(a) { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; +} + +.chat-text :where(a:hover) { + opacity: 0.8; +} + +.chat-text :where(code) { + font-family: var(--mono); + font-size: 0.9em; +} + +.chat-text :where(:not(pre) > code) { + background: rgba(0, 0, 0, 0.15); + padding: 0.15em 0.4em; + border-radius: 4px; +} + +.chat-text :where(pre) { + background: rgba(0, 0, 0, 0.15); + border-radius: 6px; + padding: 10px 12px; + overflow-x: auto; +} + +.chat-text :where(pre code) { + background: none; + padding: 0; +} + +.chat-text :where(blockquote) { + border-left: 3px solid var(--border); + padding-left: 12px; + color: var(--muted); +} + +.chat-text :where(hr) { + border: none; + border-top: 1px solid var(--border); + margin: 1em 0; +} + +/* ============================================= + GROUPED CHAT LAYOUT (Slack-style) + ============================================= */ + +/* Chat Group Layout - default (assistant/other on left) */ +.chat-group { + display: flex; + gap: 12px; + align-items: flex-start; + margin-bottom: 16px; + margin-left: 16px; + margin-right: 16px; +} + +/* User messages on right */ +.chat-group.user { + flex-direction: row-reverse; + justify-content: flex-start; +} + +.chat-group-messages { + display: flex; + flex-direction: column; + gap: 2px; + max-width: min(900px, calc(100% - 60px)); +} + +/* User messages align content right */ +.chat-group.user .chat-group-messages { + align-items: flex-end; +} + +.chat-group.user .chat-group-footer { + justify-content: flex-end; +} + +/* Footer at bottom of message group (role + time) */ +.chat-group-footer { + display: flex; + gap: 8px; + align-items: baseline; + margin-top: 6px; +} + +.chat-sender-name { + font-weight: 500; + font-size: 12px; + color: var(--muted); +} + +.chat-group-timestamp { + font-size: 11px; + color: var(--muted); + opacity: 0.7; +} + +/* Avatar Styles */ +.chat-avatar { + width: 40px; + height: 40px; + border-radius: 8px; + background: var(--panel-strong); + display: grid; + place-items: center; + font-weight: 600; + font-size: 14px; + flex-shrink: 0; + align-self: flex-end; /* Align with last message in group */ + margin-bottom: 4px; /* Optical alignment */ +} + +.chat-avatar.user { + background: rgba(245, 159, 74, 0.2); + color: rgba(245, 159, 74, 1); +} + +.chat-avatar.assistant { + background: rgba(52, 199, 183, 0.2); + color: rgba(52, 199, 183, 1); +} + +.chat-avatar.other { + background: rgba(150, 150, 150, 0.2); + color: rgba(150, 150, 150, 1); +} + +/* Minimal Bubble Design - dynamic width based on content */ +.chat-bubble { + display: inline-block; + border: 1px solid var(--border); + background: rgba(0, 0, 0, 0.12); + border-radius: 12px; + padding: 10px 14px; + box-shadow: none; + transition: background 150ms ease-out, border-color 150ms ease-out; + max-width: 100%; + word-wrap: break-word; +} + +.chat-bubble:hover { + background: rgba(0, 0, 0, 0.18); +} + +/* User bubbles have different styling */ +.chat-group.user .chat-bubble { + background: rgba(245, 159, 74, 0.15); + border-color: rgba(245, 159, 74, 0.3); +} + +.chat-group.user .chat-bubble:hover { + background: rgba(245, 159, 74, 0.22); +} + +/* Streaming animation */ +.chat-bubble.streaming { + animation: pulsing-border 1.5s ease-out infinite; +} + +@keyframes pulsing-border { + 0%, 100% { + border-color: var(--border); + } + 50% { + border-color: var(--accent); + } +} + +/* Fade-in animation for new messages */ +.chat-bubble.fade-in { + animation: fade-in 200ms ease-out; +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Tool Card Styles */ +.chat-tool-card { + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px; + margin-top: 8px; + 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(--accent); + background: rgba(0, 0, 0, 0.06); +} + +/* First tool card in a group - no top margin */ +.chat-tool-card:first-child { + margin-top: 0; +} + +.chat-tool-card--clickable { + cursor: pointer; +} + +.chat-tool-card--clickable:focus { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +/* Header with title and chevron */ +.chat-tool-card__header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.chat-tool-card__title { + display: inline-flex; + align-items: center; + gap: 6px; + font-weight: 600; + font-size: 13px; + line-height: 1.2; +} + +.chat-tool-card__icon { + display: inline-flex; + align-items: center; + justify-content: center; + 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; +} + +/* "View >" action link */ +.chat-tool-card__action { + font-size: 12px; + color: var(--accent); + opacity: 0.8; + transition: opacity 150ms ease-out; +} + +.chat-tool-card--clickable:hover .chat-tool-card__action { + opacity: 1; +} + +/* Status indicator for completed/empty results */ +.chat-tool-card__status { + font-size: 14px; + color: var(--ok); +} + +.chat-tool-card__status-text { + font-size: 11px; + margin-top: 4px; +} + +.chat-tool-card__detail { + font-size: 12px; + color: var(--muted); + margin-top: 4px; +} + +/* Collapsed preview - fixed height with truncation */ +.chat-tool-card__preview { + font-size: 11px; + color: var(--muted); + margin-top: 8px; + padding: 8px 10px; + 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 rgba(255, 255, 255, 0.04); +} + +.chat-tool-card--clickable:hover .chat-tool-card__preview { + background: rgba(0, 0, 0, 0.12); + border-color: rgba(255, 255, 255, 0.08); +} + +/* Short inline output */ +.chat-tool-card__inline { + font-size: 11px; + color: var(--text); + margin-top: 6px; + padding: 6px 8px; + background: rgba(0, 0, 0, 0.06); + border-radius: 4px; + white-space: pre-wrap; + word-break: break-word; +} + +/* Reading Indicator */ +.chat-reading-indicator { + background: transparent; + border: 1px solid var(--border); + padding: 12px; + display: inline-flex; +} + +.chat-reading-indicator__dots { + display: flex; + gap: 6px; + align-items: center; +} + +.chat-reading-indicator__dots span { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--muted); + animation: reading-pulse 1.4s ease-in-out infinite; +} + +.chat-reading-indicator__dots span:nth-child(1) { + animation-delay: 0s; +} + +.chat-reading-indicator__dots span:nth-child(2) { + animation-delay: 0.2s; +} + +.chat-reading-indicator__dots span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes reading-pulse { + 0%, 60%, 100% { + opacity: 0.3; + transform: scale(0.8); + } + 30% { + opacity: 1; + transform: scale(1); + } +} + +/* Split View Layout */ +.chat-split-container { + display: flex; + gap: 0; + flex: 1; + min-height: 0; + height: 100%; +} + +.chat-main { + min-width: 400px; + display: flex; + flex-direction: column; + overflow: hidden; + /* Smooth transition when sidebar opens/closes */ + transition: flex 250ms ease-out; +} + +.chat-sidebar { + flex: 1; + min-width: 300px; + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; + animation: slide-in 200ms ease-out; +} + +@keyframes slide-in { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Sidebar Panel */ +.sidebar-panel { + display: flex; + flex-direction: column; + height: 100%; + background: var(--panel); +} + +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 10; + background: var(--panel); +} + +/* Smaller close button for sidebar */ +.sidebar-header .btn { + padding: 4px 8px; + font-size: 14px; + min-width: auto; + line-height: 1; +} + +.sidebar-title { + font-weight: 600; + font-size: 14px; +} + +.sidebar-content { + flex: 1; + overflow: auto; + padding: 16px; +} + +.sidebar-markdown { + font-size: 14px; + line-height: 1.5; +} + +.sidebar-markdown pre { + background: rgba(0, 0, 0, 0.12); + border-radius: 4px; + padding: 12px; + overflow-x: auto; +} + +.sidebar-markdown code { + font-family: var(--mono); + font-size: 13px; +} + +/* Mobile: Full-screen modal */ +@media (max-width: 768px) { + .chat-split-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + } + + .chat-main { + display: none; /* Hide chat on mobile when sidebar open */ + } + + .chat-sidebar { + width: 100%; + min-width: 0; + border-left: none; + } +} diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 58965472e..71a785b21 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1,3 +1,5 @@ +@import './chat.css'; + .card { border: 1px solid var(--border); background: linear-gradient(160deg, rgba(255, 255, 255, 0.04), transparent 65%), @@ -210,6 +212,17 @@ background: rgba(255, 107, 107, 0.18); } +.btn--sm { + padding: 5px 10px; + font-size: 12px; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + .field { display: grid; gap: 6px; @@ -287,8 +300,9 @@ :root[data-theme="light"] .field input, :root[data-theme="light"] .field textarea, :root[data-theme="light"] .field select { - background: rgba(255, 255, 255, 0.9); - border-color: var(--border-strong); + 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, @@ -297,6 +311,26 @@ background: #ffffff; } +/* Light theme button overrides */ +:root[data-theme="light"] .btn { + 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: rgba(255, 255, 255, 1); + border-color: rgba(16, 24, 40, 0.3); +} + +:root[data-theme="light"] .btn.primary { + background: rgba(245, 159, 74, 0.15); +} + +:root[data-theme="light"] .btn.active { + background: rgba(245, 159, 74, 0.12); +} + .muted { color: var(--muted); } @@ -569,6 +603,7 @@ .shell--chat .chat { flex: 1; + max-height: calc(100vh - 180px); /* Constrain height for sticky compose */ } .chat-header { @@ -603,26 +638,18 @@ flex-direction: column; gap: 12px; flex: 1; - max-height: none; - overflow: visible; + min-height: 0; /* Allow flex shrinking for scroll behavior */ + overflow-y: auto; /* Enable scrolling */ + overflow-x: hidden; padding: 14px 12px; min-width: 0; - border-radius: 16px; - border: 1px solid var(--border); - background: linear-gradient( - 180deg, - rgba(0, 0, 0, 0.2) 0%, - rgba(0, 0, 0, 0.3) 100% - ); + border-radius: 0; + border: none; + background: transparent; } :root[data-theme="light"] .chat-thread { - border-color: rgba(16, 24, 40, 0.12); - background: linear-gradient( - 180deg, - rgba(16, 24, 40, 0.03) 0%, - rgba(16, 24, 40, 0.06) 100% - ); + background: transparent; } .chat-queue { diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index df6f39c90..75c2f4cd0 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -1,28 +1,28 @@ .shell { - --shell-pad: 18px; - --shell-gap: 18px; - --shell-nav-col: minmax(220px, 280px); - --shell-topbar-row: auto; + --shell-pad: 16px; + --shell-gap: 16px; + --shell-nav-width: 220px; + --shell-topbar-height: 56px; --shell-focus-duration: 220ms; --shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1); min-height: 100vh; display: grid; - grid-template-columns: var(--shell-nav-col) minmax(0, 1fr); - grid-template-rows: var(--shell-topbar-row) 1fr; + grid-template-columns: var(--shell-nav-width) minmax(0, 1fr); + grid-template-rows: var(--shell-topbar-height) 1fr; grid-template-areas: "topbar topbar" "nav content"; - gap: var(--shell-gap); - padding: var(--shell-pad); + gap: 0; animation: dashboard-enter 0.6s ease-out; - transition: padding var(--shell-focus-duration) var(--shell-focus-ease); + transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease); +} + +.shell--nav-collapsed { + grid-template-columns: 0px minmax(0, 1fr); } .shell--chat-focus { - --shell-pad: 8px; - --shell-gap: 0px; - --shell-nav-col: 0px; - --shell-topbar-row: auto; + grid-template-columns: 0px minmax(0, 1fr); } .shell--chat-focus .content { @@ -33,89 +33,147 @@ .topbar { grid-area: topbar; position: sticky; - top: var(--shell-pad); - z-index: 20; + top: 0; + z-index: 40; display: flex; justify-content: space-between; align-items: center; - padding: 16px 20px; - border: 1px solid var(--border); - border-radius: 18px; - background: linear-gradient(135deg, var(--chrome), rgba(255, 255, 255, 0.02)); + gap: 16px; + padding: 0 20px; + height: var(--shell-topbar-height); + border-bottom: 1px solid var(--border); + background: var(--panel); backdrop-filter: blur(18px); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28); - overflow: hidden; - transform-origin: top center; - transition: opacity var(--shell-focus-duration) var(--shell-focus-ease), - 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), - border-width var(--shell-focus-duration) var(--shell-focus-ease); - max-height: max(0px, var(--topbar-height, 92px)); +} + +.topbar-left { + display: flex; + align-items: center; + gap: 12px; +} + +.topbar .nav-collapse-toggle { + width: 44px; + height: 44px; + margin-bottom: 0; +} + +.topbar .nav-collapse-toggle__icon { + font-size: 22px; } .brand { - display: grid; - gap: 4px; + display: flex; + flex-direction: column; + gap: 2px; } .brand-title { font-family: var(--font-display); - font-size: 20px; - letter-spacing: 0.6px; + font-size: 16px; + letter-spacing: 1px; text-transform: uppercase; + font-weight: 600; + line-height: 1.1; } .brand-sub { + font-size: 10px; color: var(--muted); - font-size: 12px; - letter-spacing: 1.2px; + letter-spacing: 0.8px; text-transform: uppercase; + line-height: 1; } .topbar-status { display: flex; align-items: center; - gap: 10px; + gap: 8px; +} + +/* Smaller pill and theme toggle in topbar */ +.topbar-status .pill { + padding: 4px 10px; + gap: 6px; + font-size: 11px; +} + +.topbar-status .statusDot { + width: 6px; + height: 6px; +} + +.topbar-status .theme-toggle { + --theme-item: 22px; + --theme-gap: 4px; + --theme-pad: 4px; +} + +.topbar-status .theme-icon { + width: 12px; + height: 12px; } .nav { grid-area: nav; - position: sticky; - top: calc( - var(--shell-pad) + var(--topbar-height, 0px) + var(--shell-gap) - ); - align-self: start; - max-height: calc( - 100vh - var(--topbar-height, 0px) - var(--shell-gap) - - var(--shell-pad) - var(--shell-pad) - ); - overflow: auto; + overflow-y: auto; + overflow-x: hidden; padding: 16px; - border: 1px solid var(--border); - border-radius: 20px; + border-right: 1px solid var(--border); background: var(--panel); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); - transform-origin: left center; - transition: opacity var(--shell-focus-duration) var(--shell-focus-ease), - transform var(--shell-focus-duration) var(--shell-focus-ease), - max-width var(--shell-focus-duration) var(--shell-focus-ease), - padding var(--shell-focus-duration) var(--shell-focus-ease), - border-width var(--shell-focus-duration) var(--shell-focus-ease); - max-width: 320px; + transition: width var(--shell-focus-duration) var(--shell-focus-ease), + padding var(--shell-focus-duration) var(--shell-focus-ease); } .shell--chat-focus .nav { - opacity: 0; - transform: translateX(-12px); - max-width: 0px; + width: 0; padding: 0; border-width: 0; overflow: hidden; pointer-events: none; } +/* Collapsed nav sidebar - completely hidden */ +.nav--collapsed { + width: 0; + min-width: 0; + padding: 0; + overflow: hidden; + border: none; + opacity: 0; + pointer-events: none; +} + +/* Nav collapse toggle button */ +.nav-collapse-toggle { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + cursor: pointer; + transition: background 150ms ease, border-color 150ms ease; + margin-bottom: 16px; +} + +.nav-collapse-toggle: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 { + font-size: 16px; + color: var(--muted); +} + .nav-group { margin-bottom: 18px; display: grid; @@ -130,27 +188,77 @@ border-bottom: none; } +.nav-group__items { + display: grid; + gap: 4px; +} + +.nav-group--collapsed .nav-group__items { + display: none; +} + .nav-label { - font-size: 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 4px 0; + font-size: 11px; + font-weight: 500; text-transform: uppercase; - letter-spacing: 1.6px; - color: var(--muted); + letter-spacing: 1.4px; + color: var(--text); + opacity: 0.7; + margin-bottom: 4px; + background: transparent; + border: none; + cursor: pointer; + text-align: left; +} + +.nav-label:hover { + opacity: 1; +} + +.nav-label__text { + flex: 1; +} + +.nav-label__chevron { + font-size: 12px; + opacity: 0.6; } .nav-item { position: relative; display: flex; align-items: center; - justify-content: space-between; - gap: 10px; + justify-content: flex-start; + gap: 8px; padding: 10px 12px 10px 14px; border-radius: 12px; border: 1px solid transparent; - background: rgba(255, 255, 255, 0.02); + background: transparent; color: var(--muted); cursor: pointer; - transition: border-color 160ms ease, background 160ms ease, color 160ms ease, - transform 160ms ease; + text-decoration: none; + transition: border-color 160ms ease, background 160ms ease, color 160ms ease; +} + +.nav-item__icon { + font-size: 16px; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.nav-item__text { + font-size: 13px; + white-space: nowrap; } .nav-item:hover { @@ -162,11 +270,11 @@ .nav-item::before { content: ""; position: absolute; - left: 6px; + left: 0; top: 50%; width: 4px; height: 60%; - border-radius: 999px; + border-radius: 0 999px 999px 0; transform: translateY(-50%); background: transparent; } @@ -174,8 +282,7 @@ .nav-item.active { color: var(--text); border-color: rgba(245, 159, 74, 0.45); - background: rgba(245, 159, 74, 0.16); - transform: translateX(2px); + background: rgba(245, 159, 74, 0.12); } .nav-item.active::before { @@ -190,13 +297,12 @@ flex-direction: column; gap: 20px; min-height: 0; + overflow-y: auto; /* Enable vertical scrolling for pages with long content */ + overflow-x: hidden; } .shell--chat .content { - min-height: calc( - 100vh - var(--topbar-height, 0px) - var(--shell-gap) - - var(--shell-pad) - var(--shell-pad) - ); + /* No-op: keep chat layout consistent with other tabs */ } .docs-link { @@ -264,6 +370,26 @@ gap: 10px; } +/* Chat view: header and controls side by side */ +.content--chat .content-header { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.content--chat .content-header > div:first-child { + text-align: left; +} + +.content--chat .page-meta { + justify-content: flex-start; +} + +.content--chat .chat-controls { + flex-shrink: 0; +} + .grid { display: grid; gap: 18px; diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 873f99a93..888c6db7d 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -3,6 +3,7 @@ import { html, nothing } from "lit"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway"; import { TAB_GROUPS, + iconForTab, pathForTab, subtitleForTab, titleForTab, @@ -215,11 +216,25 @@ export function renderApp(state: AppViewState) { const chatFocus = isChat && state.settings.chatFocusMode; return html` -
+
-
-
Clawdbot Control
-
Gateway dashboard
+
+ +
+
CLAWDBOT
+
Gateway Dashboard
+
@@ -227,28 +242,36 @@ export function renderApp(state: AppViewState) { Health ${state.connected ? "OK" : "Offline"}
- ${isChat - ? renderChatFocusToggle( - state.settings.chatFocusMode, - () => - state.applySettings({ - ...state.settings, - chatFocusMode: !state.settings.chatFocusMode, - }), - ) - : nothing} ${renderThemeToggle(state)}
-