From 60c2ee1f1d1122c4f9529cf96bca747967e76edd Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Sun, 26 Jul 2026 14:21:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20WebUI=20modernization=20PR1=20?= =?UTF-8?q?=E2=80=94=20hygiene,=20design=20tokens,=20type=20floor,=20self-?= =?UTF-8?q?hosted=20font?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First PR of the WebUI modernization roadmap (Phase 0 + Phase 1 foundation). Phase 0 — hygiene: - Remove dead code: initParticles (targeted nonexistent #particles), addTypingIndicator (never called), #stats-bar element + writes, .li-spinner legacy CSS, dead .approval-dialog fallback branch. - Deduplicate: extract resetTurnState() from the three identical streaming-state reset blocks (send / newSession / loadAndRenderSession); extract ensureSessionToken() from the two identical session-token bootstrap blocks (delete / rename); unify clipboard writes behind copyTextToClipboard() with execCommand fallback. - Micro-bugs: collapse threshold mismatch (JS measured 500px, CSS capped 460px — content could clip with no expander; now shared constant 460), escapeAttr now also escapes < and >, tb- class-prefix collision between topbar and tool blocks resolved (topbar renamed to top-*), Mesage typo. Phase 1 — design system foundation: - Token scales in :root: type (--fs-xs..xl, 11px floor), spacing (--space-1..8, 4px grid), radius (--r-sm/md/lg). - 11px type floor: every font-size below 11px (42 declarations down to 8px) raised to 11px for WCAG AA readability. - Contrast: --text-3 lifted from #706e68 to #8a8880 (~5.5:1 on the carbon background) so tertiary text passes AA at the new floor size. - Self-hosted font: Azeret Mono variable woff2 (26 KB, weights 100-700) embedded at ui/fonts/ and served at /fonts/azeret-mono.woff2 via the static file map; Google Fonts CDN links removed - the UI now works fully offline/air-gapped. Verified: node --check, go vet, full cmd/odek test suite, and a live smoke test of odek serve (/, /style.css, /app.js, /fonts/azeret-mono.woff2 all 200). No behavioral changes beyond readability improvements. --- cmd/odek/serve.go | 3 + cmd/odek/ui/app.js | 171 ++++++++++++---------------- cmd/odek/ui/fonts/azeret-mono.woff2 | Bin 0 -> 26216 bytes cmd/odek/ui/index.html | 13 +-- cmd/odek/ui/style.css | 149 ++++++++++++++---------- 5 files changed, 168 insertions(+), 168 deletions(-) create mode 100644 cmd/odek/ui/fonts/azeret-mono.woff2 diff --git a/cmd/odek/serve.go b/cmd/odek/serve.go index b23d3e2c..ff9c6bf1 100644 --- a/cmd/odek/serve.go +++ b/cmd/odek/serve.go @@ -1748,6 +1748,9 @@ var staticFiles = map[string][2]string{ "/": {"ui/index.html", "text/html; charset=utf-8"}, "/style.css": {"ui/style.css", "text/css; charset=utf-8"}, "/app.js": {"ui/app.js", "application/javascript; charset=utf-8"}, + // Self-hosted font (variable weight 100–700) so the UI works offline and + // does not depend on the Google Fonts CDN. + "/fonts/azeret-mono.woff2": {"ui/fonts/azeret-mono.woff2", "font/woff2"}, } func handleStatic(wsToken string) http.HandlerFunc { diff --git a/cmd/odek/ui/app.js b/cmd/odek/ui/app.js index 8dbfe91a..ac749bcf 100644 --- a/cmd/odek/ui/app.js +++ b/cmd/odek/ui/app.js @@ -39,6 +39,26 @@ function clearSessionToken(sid) { localStorage.removeItem('odek_session_token_' + sid); } +// ensureSessionToken returns a stored token for sid, bootstrapping one from +// the server (GET /api/sessions/) when missing. Used by session REST +// mutations (rename/delete) whose tokens may predate this browser. Returns +// '' on failure — the server will answer 401 if a token was required. +async function ensureSessionToken(sid) { + let token = getSessionToken(sid); + if (token) return token; + try { + const bootstrap = await fetch('/api/sessions/' + encodeURIComponent(sid), { + headers: apiHeaders() + }); + if (bootstrap.ok) { + const bs = await bootstrap.json(); + token = bootstrap.headers.get('X-Session-Token') || bs.auth_token; + if (token) setSessionToken(sid, token); + } + } catch { /* continue — server will return 401 if token required */ } + return token || ''; +} + // ── DOM ── const messagesEl = document.getElementById('messages'); const promptEl = document.getElementById('prompt'); @@ -47,7 +67,6 @@ const completionEl = document.getElementById('completion'); const statusEl = document.getElementById('ws-status'); const dotEl = document.getElementById('ws-dot'); const modelLabel = document.getElementById('model-label'); -const statsBar = document.getElementById('stats-bar'); const sessionListEl = document.getElementById('session-list'); const sidebarSearch = document.getElementById('sidebar-search'); const emptyState = document.getElementById('empty-state'); @@ -73,6 +92,24 @@ let inToolGroup = false; // Timestamps for tool latency (name → start ms, queue-based like above). const toolStartQueues = new Map(); +// resetTurnState clears all per-turn streaming/tool/sub-agent state. Called +// before a new turn (send), on new session, and when loading a session. +function resetTurnState() { + streamBuffer = ''; + if (streamRAF) { + cancelAnimationFrame(streamRAF); + streamRAF = null; + } + streamBubbleEl = null; + streamContentEl = null; + currentToolBlock = null; + subagentGroup = null; + thinkingContentEl = null; + toolBlockQueues.clear(); + toolStartQueues.clear(); + inToolGroup = false; +} + // ── Sub-agent state ── let subagentGroup = null; @@ -173,10 +210,10 @@ function formatNum(n) { return String(n); } -// ── Mesage cap ── +// ── Message cap ── const MAX_MESSAGES = 80; function pruneMessages() { - const items = messagesEl.querySelectorAll(':scope > .msg, :scope > .tool-block, :scope > .subagent-group, :scope > .thinking-block, :scope > .typing-indicator'); + const items = messagesEl.querySelectorAll(':scope > .msg, :scope > .tool-block, :scope > .subagent-group, :scope > .thinking-block'); if (items.length > MAX_MESSAGES) { for (let i = 0, n = items.length - MAX_MESSAGES; i < n; i++) { items[i].remove(); @@ -190,22 +227,6 @@ promptEl.addEventListener('input', () => { promptEl.style.height = Math.min(promptEl.scrollHeight, 200) + 'px'; }); -// ── Particles ── -function initParticles() { - const el = document.getElementById('particles'); - if (!el) return; - for (let i = 0; i < 15; i++) { - const p = document.createElement('div'); - p.className = 'particle'; - p.style.left = Math.random() * 100 + '%'; - p.style.animationDelay = Math.random() * 6 + 's'; - p.style.animationDuration = (4 + Math.random() * 4) + 's'; - p.style.width = p.style.height = (1 + Math.random() * 3) + 'px'; - el.appendChild(p); - } -} -initParticles(); - // ── WebSocket ── function getWsToken() { const meta = document.querySelector('meta[name="odek-ws-token"]'); @@ -233,7 +254,6 @@ function connect() { dotEl.className = 'dot connected'; statusEl.textContent = 'connected'; sendBtn.disabled = false; - statsBar.textContent = ''; // Hide loading skeleton when connected if (skeletonEl) skeletonEl.classList.remove('visible'); }; @@ -538,15 +558,6 @@ function addSystemMessage(content) { scrollBottom(); } -function addTypingIndicator() { - const el = document.createElement('div'); - el.className = 'typing-indicator'; - el.innerHTML = '
'; - messagesEl.appendChild(el); - scrollBottom(); - return el; -} - function addDivider(text) { const el = document.createElement('div'); el.className = 'msg-divider'; @@ -906,8 +917,7 @@ window.showApprovalDialog = function(event) { frictionMsg = document.createElement('div'); frictionMsg.id = 'approval-friction-msg'; frictionMsg.style.cssText = 'color: var(--accent); font-size: 13px; margin-top: 8px;'; - overlay.querySelector('.approval-dialog')?.appendChild(frictionMsg) - || document.getElementById('approval-actions').parentNode.insertBefore(frictionMsg, document.getElementById('approval-actions')); + document.getElementById('approval-actions').parentNode.insertBefore(frictionMsg, document.getElementById('approval-actions')); } frictionMsg.textContent = `⚠️ You have approved ${event.friction_approvals || 0} ${event.risk} operations in the last minute. ` + @@ -968,19 +978,7 @@ window.executeDeleteSession = async function() { pendingDeleteId = null; document.getElementById('confirm-overlay').classList.remove('active'); - let token = getSessionToken(sid); - if (!token) { - try { - const bootstrap = await fetch('/api/sessions/' + encodeURIComponent(sid), { - headers: apiHeaders() - }); - if (bootstrap.ok) { - const bs = await bootstrap.json(); - token = bootstrap.headers.get('X-Session-Token') || bs.auth_token; - if (token) setSessionToken(sid, token); - } - } catch { /* continue — server will return 401 if token required */ } - } + const token = await ensureSessionToken(sid); fetch('/api/sessions/' + encodeURIComponent(sid), { method: 'DELETE', @@ -1114,19 +1112,7 @@ window.renameSession = async function(sid, el) { const newName = prompt('Rename session:', currentName); if (!newName || newName === currentName) return; - let token = getSessionToken(sid); - if (!token) { - try { - const bootstrap = await fetch('/api/sessions/' + encodeURIComponent(sid), { - headers: apiHeaders() - }); - if (bootstrap.ok) { - const bs = await bootstrap.json(); - token = bootstrap.headers.get('X-Session-Token') || bs.auth_token; - if (token) setSessionToken(sid, token); - } - } catch { /* continue — server will return 401 if token required */ } - } + const token = await ensureSessionToken(sid); fetch('/api/sessions/' + encodeURIComponent(sid), { method: 'POST', @@ -1229,11 +1215,7 @@ window.newSession = function() { sessionId = null; // Reset all streaming + tool state. - streamBuffer = ''; - if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = null; } - streamBubbleEl = null; streamContentEl = null; - currentToolBlock = null; subagentGroup = null; thinkingContentEl = null; - toolBlockQueues.clear(); toolStartQueues.clear(); inToolGroup = false; + resetTurnState(); busy = false; hideLoading(); hideCancel(); sendBtn.disabled = !ws || ws.readyState !== WebSocket.OPEN; @@ -1276,7 +1258,8 @@ function escapeAttr(s) { if (!s) return ''; // & must be replaced first — doing it last double-escapes the entities // introduced by the quote replacements (" → &quot;). - return s.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,'''); + return s.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''') + .replace(//g,'>'); } // ── Markdown to HTML (safe, no DOMPurify needed since we control input) ── @@ -1346,30 +1329,38 @@ function markdownToHtml(text) { return html; } -// ── Copy code ── +// ── Copy helpers ── +// copyTextToClipboard writes text to the clipboard, falling back to the +// legacy execCommand path when the async Clipboard API is unavailable or +// denied (non-secure contexts). Returns a Promise. +function copyTextToClipboard(text) { + const fallback = () => { + const ta = document.createElement('textarea'); + ta.value = text; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + }; + if (!navigator.clipboard || !navigator.clipboard.writeText) { + fallback(); + return Promise.resolve(); + } + return navigator.clipboard.writeText(text).catch(fallback); +} + window.copyCode = function(el) { const block = el.closest('.code-block'); if (!block) return; const code = block.querySelector('pre code'); if (!code) return; - const text = code.textContent; - navigator.clipboard.writeText(text).then(() => { + copyTextToClipboard(code.textContent).then(() => { el.textContent = '✓ copied'; el.classList.add('copied'); setTimeout(() => { el.textContent = '📋 copy'; el.classList.remove('copied'); }, 2000); - }).catch(() => { - // Fallback - const ta = document.createElement('textarea'); - ta.value = text; - document.body.appendChild(ta); - ta.select(); - document.execCommand('copy'); - document.body.removeChild(ta); - el.textContent = '✓ copied'; - setTimeout(() => { el.textContent = '📋 copy'; }, 2000); }); }; @@ -1403,19 +1394,7 @@ function send() { addMessage('user', display); // Reset streaming + tool state for the new turn. - streamBuffer = ''; - if (streamRAF) { - cancelAnimationFrame(streamRAF); - streamRAF = null; - } - streamBubbleEl = null; - streamContentEl = null; - currentToolBlock = null; - subagentGroup = null; - thinkingContentEl = null; - toolBlockQueues.clear(); - toolStartQueues.clear(); - inToolGroup = false; + resetTurnState(); busy = true; sendBtn.disabled = true; @@ -1769,11 +1748,7 @@ async function loadAndRenderSession(sid) { sessionId = sid; // Clear current messages and reset all streaming state. - streamBuffer = ''; - if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = null; } - streamBubbleEl = null; streamContentEl = null; - currentToolBlock = null; subagentGroup = null; thinkingContentEl = null; - toolBlockQueues.clear(); toolStartQueues.clear(); inToolGroup = false; + resetTurnState(); busy = false; hideLoading(); hideCancel(); sendBtn.disabled = !ws || ws.readyState !== WebSocket.OPEN; promptEl.disabled = false; @@ -1972,14 +1947,14 @@ window.copyMessage = function(btn, content) { } } if (!content) return; - navigator.clipboard.writeText(content).then(function() { + copyTextToClipboard(content).then(function() { btn.classList.add('copied'); btn.innerHTML = ' Copied'; setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = ''; }, 2000); - }).catch(function(){}); + }); }; // ── Phase 1: Add copy buttons to rendered messages ── @@ -1994,9 +1969,11 @@ function addCopyButton(bubble) { } // ── Phase 1: Collapse long messages after rendering ── +// Must match the max-height of .bubble.collapsible in style.css. +const COLLAPSE_MAX_HEIGHT_PX = 460; function checkCollapse(bubble) { var content = bubble.querySelector('.content'); - if (!content || content.scrollHeight <= 500) return; + if (!content || content.scrollHeight <= COLLAPSE_MAX_HEIGHT_PX) return; bubble.classList.add('collapsible'); var toggle = document.createElement('div'); toggle.className = 'collapse-toggle'; diff --git a/cmd/odek/ui/fonts/azeret-mono.woff2 b/cmd/odek/ui/fonts/azeret-mono.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8b71c2eb6c695fb1d146688bbe28c31ef54f09a5 GIT binary patch literal 26216 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 odek ⚡ - - - +
@@ -17,17 +15,16 @@ - odek - + odek + connecting -
-
+
-
+
diff --git a/cmd/odek/ui/style.css b/cmd/odek/ui/style.css index e4c6cdf2..6e6ebda3 100644 --- a/cmd/odek/ui/style.css +++ b/cmd/odek/ui/style.css @@ -4,6 +4,15 @@ Incandescent amber on deep carbon. Brutally precise. ═══════════════════════════════════════════════════════════════════════ */ +/* Self-hosted variable font (weights 100–700) — no CDN dependency, works offline */ +@font-face { + font-family: 'Azeret Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url('/fonts/azeret-mono.woff2') format('woff2'); +} + /* ── Design tokens ─────────────────────────────────────────────────── */ :root { /* Carbon palette */ @@ -18,7 +27,7 @@ /* Text — calibrated for #060608 background */ --text: #f0ede6; /* primary — full brightness */ --text-2: #a8a49a; /* secondary — was #78756e, lifted for readability */ - --text-3: #706e68; /* tertiary — was #3e3c38 (~2.8:1), now ~4.6:1 */ + --text-3: #8a8880; /* tertiary — lifted from #706e68 to ~5.5:1 for WCAG AA at 11px */ /* Amber accent */ --amber: #c8860a; @@ -35,6 +44,30 @@ --ui: 'Azeret Mono', 'JetBrains Mono', 'Fira Mono', monospace; --code: 'Azeret Mono', 'JetBrains Mono', 'Fira Mono', monospace; + /* Type scale — --fs-xs (11px) is the floor for any text (WCAG AA); + icon glyphs may go smaller only when purely decorative */ + --fs-xs: 11px; + --fs-sm: 12px; + --fs-base: 13px; + --fs-md: 14px; + --fs-lg: 18px; + --fs-xl: 24px; + + /* Spacing scale (4px grid) */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-7: 48px; + --space-8: 64px; + + /* Radius scale */ + --r-sm: 3px; + --r-md: 6px; + --r-lg: 12px; + /* Motion — one humanist ease everywhere, one snappy ease for press feedback */ --ease: cubic-bezier(.2, .7, .2, 1); --ease-snap: cubic-bezier(.4, 0, .2, 1); @@ -107,7 +140,6 @@ body.light { height: 100vh; } -/* ── Topbar ────────────────────────────────────────────────────────── */ /* ── Topbar ────────────────────────────────────────────────────────── */ #topbar { grid-column: 1 / -1; @@ -130,13 +162,13 @@ body.light { } /* Mark + brand wordmark */ -.tb-brand { +.top-brand { display: flex; align-items: center; gap: 7px; flex-shrink: 0; } -.tb-brand::before { +.top-brand::before { content: ''; display: inline-block; width: 7px; @@ -148,11 +180,11 @@ body.light { flex-shrink: 0; transition: transform .5s var(--ease-spring), box-shadow .3s var(--ease); } -.tb-brand:hover::before { +.top-brand:hover::before { transform: rotate(102deg) scale(1.15); box-shadow: 0 0 14px rgba(200,134,10,.85); } -.tb-brand-text { +.top-brand-text { font-size: 13px; font-weight: 600; color: var(--text); @@ -161,7 +193,7 @@ body.light { } /* Status group */ -.tb-status-group { +.top-status-group { display: flex; align-items: center; gap: 7px; @@ -185,7 +217,7 @@ body.light { .status { color: var(--text-2); - font-size: 10px; + font-size: 11px; letter-spacing: .05em; text-transform: lowercase; } @@ -198,10 +230,10 @@ body.light { } #sandbox-badge:hover { opacity: 1; } -.tb-spacer { flex: 1; } +.top-spacer { flex: 1; } /* Right-side controls group: slight separation from content */ -.tb-controls { +.top-controls { display: flex; align-items: center; gap: 8px; @@ -209,14 +241,14 @@ body.light { border-left: 1px solid var(--border-2); } -.stats, .model { - font-size: 10px; +.model { + font-size: 11px; color: var(--text-2); letter-spacing: .02em; } #session-stats { - font-size: 10px; + font-size: 11px; color: var(--text-2); padding-left: 10px; border-left: 1px solid var(--border-2); @@ -226,7 +258,7 @@ body.light { #model-picker { font-family: var(--ui); - font-size: 10px; + font-size: 11px; background: var(--bg-3); border: 1px solid var(--border-2); border-radius: 3px; @@ -246,7 +278,7 @@ body.light { #custom-model-input { font-family: var(--ui); - font-size: 10px; + font-size: 11px; background: var(--bg-3); border: 1px solid var(--amber); border-radius: 3px; @@ -282,7 +314,7 @@ body.light { color: var(--red); cursor: pointer; font-family: var(--ui); - font-size: 10px; + font-size: 11px; font-weight: 500; padding: 3px 10px; letter-spacing: .04em; @@ -340,7 +372,7 @@ body.light { margin-bottom: 2px; } #sidebar-header h3 { - font-size: 9px; + font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .18em; @@ -373,7 +405,7 @@ body.light { border-radius: 4px; color: var(--text); font-family: var(--ui); - font-size: 10.5px; + font-size: 11px; outline: none; letter-spacing: .02em; transition: border-color .22s var(--ease), background .22s var(--ease), box-shadow .22s var(--ease); @@ -436,7 +468,7 @@ body.light { /* Session ID line — de-emphasised */ .session-item .id { font-family: var(--ui); - font-size: 8.5px; + font-size: 11px; color: var(--text-3); letter-spacing: .07em; text-transform: uppercase; @@ -455,7 +487,7 @@ body.light { .session-item .task.untitled { font-style: italic; opacity: 0.55; } .session-item .meta { - font-size: 9px; + font-size: 11px; color: var(--text-3); margin-top: 4px; display: flex; @@ -468,7 +500,7 @@ body.light { border: 1px solid rgba(200,134,10,.14); padding: 0 4px; border-radius: 2px; - font-size: 8px; + font-size: 11px; color: rgba(200,134,10,.7); letter-spacing: .04em; } @@ -480,7 +512,7 @@ body.light { .session-item .del-btn, .session-item .rename-btn { opacity: 0; transition: opacity .12s; - font-size: 10px; + font-size: 11px; color: var(--text-3); cursor: pointer; padding: 0 3px; @@ -576,7 +608,7 @@ body.light { animation: esSubFade 1.4s var(--ease) both; } .es-hints span { - font-size: 10px; + font-size: 11px; color: var(--text-3); cursor: pointer; letter-spacing: .06em; @@ -664,7 +696,7 @@ body.light { background: transparent; border: none; color: var(--text-3); - font-size: 10px; + font-size: 11px; letter-spacing: .04em; padding: 4px 12px; text-transform: lowercase; @@ -672,7 +704,7 @@ body.light { /* Sender label */ .bubble .sender { - font-size: 9px; + font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .14em; @@ -768,7 +800,7 @@ body.light { background: rgba(255,255,255,.02); border-bottom: 1px solid var(--border); font-family: var(--ui); - font-size: 9px; + font-size: 11px; color: var(--text-3); text-transform: uppercase; letter-spacing: .1em; @@ -794,7 +826,7 @@ body.light { color: var(--text-3); cursor: pointer; font-family: var(--ui); - font-size: 9px; + font-size: 11px; text-transform: uppercase; letter-spacing: .1em; padding: 4px 8px; @@ -830,7 +862,7 @@ body.light { display: flex; align-items: center; gap: 12px; - font-size: 9px; + font-size: 11px; color: var(--text-3); letter-spacing: .12em; text-transform: uppercase; @@ -876,17 +908,17 @@ body.light { pointer-events: none; } .tb-emoji { font-size: 12px; flex-shrink: 0; opacity: .7; } -.tb-name { color: var(--amber-2); font-weight: 500; font-size: 10.5px; letter-spacing: .04em; } +.tb-name { color: var(--amber-2); font-weight: 500; font-size: 11px; letter-spacing: .04em; } .tb-preview { color: var(--text-3); - font-size: 10px; + font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; flex-shrink: 1; } -.arrow { transition: transform .18s; font-size: 8px; color: var(--text-3); flex-shrink: 0; font-style: normal; } +.arrow { transition: transform .18s; font-size: 11px; color: var(--text-3); flex-shrink: 0; font-style: normal; } .arrow.open { transform: rotate(90deg); } .tb-spinner { display: none; @@ -899,7 +931,7 @@ body.light { } .tb-spinner.running { display: inline-block; } @keyframes spin { to { transform: rotate(360deg); } } -.tb-latency { font-size: 9px; color: var(--text-3); margin-left: auto; flex-shrink: 0; letter-spacing: .04em; } +.tb-latency { font-size: 11px; color: var(--text-3); margin-left: auto; flex-shrink: 0; letter-spacing: .04em; } .tb-body { display: none; @@ -925,7 +957,7 @@ body.light { border-radius: 3px; padding: 8px 12px; font-family: var(--ui); - font-size: 10.5px; + font-size: 11px; color: var(--text-2); white-space: pre-wrap; max-height: 160px; @@ -935,7 +967,7 @@ body.light { .tb-result-more { color: var(--amber); cursor: pointer; - font-size: 10px; + font-size: 11px; display: block; margin-top: 4px; letter-spacing: .04em; @@ -945,7 +977,7 @@ body.light { /* ── Sub-agent cards ───────────────────────────────────────────────── */ .subagent-group { margin: 12px 0; } .sg-header { - font-size: 9px; + font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .12em; @@ -965,22 +997,22 @@ body.light { .subagent-card.completed { border-left-color: var(--green); } .subagent-card.error { border-left-color: var(--red); } .sa-top { display: flex; align-items: center; gap: 8px; } -.sa-icon { width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; flex-shrink: 0; } +.sa-icon { width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; flex-shrink: 0; } .subagent-card.pending .sa-icon { background: var(--border); color: var(--text-3); } .subagent-card.running .sa-icon { background: var(--amber-dim); color: var(--amber-2); animation: pulseSa 1.5s ease-in-out infinite; } .subagent-card.completed .sa-icon { background: rgba(46,184,122,.15); color: var(--green); } .subagent-card.error .sa-icon { background: rgba(224,82,82,.15); color: var(--red); } @keyframes pulseSa { 0%,100% { opacity: 1; } 50% { opacity: .55; } } .sa-goal { flex: 1; font-size: 12px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.sa-status { font-size: 9px; color: var(--text-3); flex-shrink: 0; letter-spacing: .04em; text-transform: lowercase; } +.sa-status { font-size: 11px; color: var(--text-3); flex-shrink: 0; letter-spacing: .04em; text-transform: lowercase; } .subagent-card.running .sa-status { color: var(--amber); } .subagent-card.completed .sa-status { color: var(--green); } .subagent-card.error .sa-status { color: var(--red); } .sa-details { display: none; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); } .sa-details.open { display: block; animation: fadeIn .15s ease-out; } -.sa-meta { font-size: 9px; color: var(--text-3); margin-bottom: 5px; letter-spacing: .04em; } +.sa-meta { font-size: 11px; color: var(--text-3); margin-bottom: 5px; letter-spacing: .04em; } .sa-summary { font-size: 11px; color: var(--text-2); line-height: 1.55; white-space: pre-wrap; } -.sa-log .log-line { font-size: 10px; color: var(--text-3); line-height: 1.5; } +.sa-log .log-line { font-size: 11px; color: var(--text-3); line-height: 1.5; } /* ── Streaming cursor — incandescent amber bar with soft glow ─────── */ .stream-cursor { @@ -1031,18 +1063,9 @@ body.light { 0%, 80%, 100% { transform: scale(.5); opacity: .4; } 40% { transform: scale(1); opacity: 1; } } -.li-spinner { - /* legacy spinner kept for backward compat with any other call site */ - width: 12px; height: 12px; - border: 1.5px solid rgba(200,134,10,.2); - border-top-color: var(--amber); - border-radius: 50%; - animation: spin .7s linear infinite; - flex-shrink: 0; -} .li-text { font-family: var(--ui); - font-size: 10.5px; + font-size: 11px; color: var(--text-3); letter-spacing: .08em; text-transform: lowercase; @@ -1061,7 +1084,7 @@ body.light { border: 1px solid rgba(46,184,122,.14); border-radius: 3px; font-family: var(--ui); - font-size: 10px; + font-size: 11px; color: var(--text-2); letter-spacing: .02em; animation: arrive .18s ease-out both; @@ -1070,7 +1093,7 @@ body.light { /* ── Message stats ─────────────────────────────────────────────────── */ .msg-stats { font-family: var(--ui); - font-size: 9.5px; + font-size: 11px; color: var(--text-3); margin-top: 10px; padding-top: 8px; @@ -1101,7 +1124,7 @@ body.light { background: var(--bg-3); border: 1px solid var(--border-2); font-family: var(--ui); - font-size: 9px; + font-size: 11px; color: var(--text-3); letter-spacing: .04em; display: flex; @@ -1124,7 +1147,7 @@ body.light { cursor: pointer; padding: 5px 0; font-family: var(--ui); - font-size: 9px; + font-size: 11px; letter-spacing: .08em; text-transform: lowercase; color: var(--amber); @@ -1203,7 +1226,7 @@ body.light { border-radius: 3px; padding: 3px 4px 3px 8px; font-family: var(--ui); - font-size: 10.5px; + font-size: 11px; color: var(--amber-2); animation: chipIn .22s var(--ease-spring); transition: border-color .15s var(--ease), background .15s var(--ease); @@ -1215,7 +1238,7 @@ body.light { } .chip-icon { font-size: 11px; opacity: .9; line-height: 1; } .chip-name { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); } -.chip-size { color: var(--text-3); font-size: 9px; letter-spacing: .04em; } +.chip-size { color: var(--text-3); font-size: 11px; letter-spacing: .04em; } .chip-remove { cursor: pointer; opacity: .45; @@ -1267,7 +1290,7 @@ body.light { padding-left: 14px; } .comp-type { - font-size: 8px; + font-size: 11px; text-transform: uppercase; letter-spacing: .1em; color: var(--amber); @@ -1277,7 +1300,7 @@ body.light { flex-shrink: 0; } .comp-label { color: var(--text); } -.comp-detail { margin-left: auto; font-size: 9px; color: var(--text-3); flex-shrink: 0; } +.comp-detail { margin-left: auto; font-size: 11px; color: var(--text-3); flex-shrink: 0; } /* Input row */ #input-row { display: flex; align-items: flex-end; gap: 8px; position: relative; } @@ -1347,7 +1370,7 @@ body.light { color: var(--text-3); cursor: pointer; font-family: var(--ui); - font-size: 9px; + font-size: 11px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; @@ -1512,14 +1535,14 @@ body.light { .approval-header { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; } .approval-icon { font-size: 22px; flex-shrink: 0; } .approval-title { font-family: var(--ui); font-size: 13px; font-weight: 500; color: var(--text); letter-spacing: .02em; } -.approval-subtitle { font-family: var(--ui); font-size: 10px; color: var(--text-3); margin-top: 1px; letter-spacing: .04em; } +.approval-subtitle { font-family: var(--ui); font-size: 11px; color: var(--text-3); margin-top: 1px; letter-spacing: .04em; } .approval-risk { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 2px; font-family: var(--ui); - font-size: 9px; + font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .1em; @@ -1600,7 +1623,7 @@ body.light { .sc-label { font-family: var(--ui); font-size: 11px; color: var(--text); } .sc-key { font-family: var(--ui); - font-size: 10px; + font-size: 11px; background: var(--bg-3); border: 1px solid var(--border-2); border-bottom-width: 2px; @@ -1694,8 +1717,8 @@ body.light .msg.assistant .bubble .content { color: var(--text); } } #sidebar.active { transform: translateX(0); } #hamburger-btn { display: flex; } - .stats, #session-stats, .model { display: none !important; } - .tb-controls { border-left: none; padding-left: 0; } + #session-stats, .model { display: none !important; } + .top-controls { border-left: none; padding-left: 0; } #cancel-btn.visible { display: inline-flex; } .es-name { font-size: 36px; } }