From 250551799bb52757eb8c5dfbe81454850bae6552 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Fri, 20 Feb 2026 18:50:51 -0800 Subject: [PATCH] style: adopt agent-market design language for web UI (#282) * fix: move Logs to status bar and fix chat history ordering after restart Move the Logs tab out of the main tab bar and into the right-side status area as a compact pill button next to "Connected". Remove it from the Ctrl+1-N shortcut order (now Ctrl+1-5). Fix chat message ordering in libSQL backend: datetime('now') has only second precision, so back-to-back user+assistant inserts got identical timestamps causing non-deterministic ORDER BY. Now passes explicit millisecond-precision timestamps and uses rowid as tiebreaker for existing data. Co-Authored-By: Claude Opus 4.6 * refactor: separate WASM extensions from MCP servers on Extensions page Reorganize the Extensions tab into 5 distinct sections: Installed Extensions, Available WASM Extensions, Install WASM Extension (by tar.gz URL), MCP Servers (with Add Custom form), and Registered Tools. Registry entries are now filtered client-side by kind so WASM tools/ channels and MCP servers each have dedicated UI sections. Co-Authored-By: Claude Opus 4.6 * style: adopt agent-market design language for web UI Refresh the web gateway visual identity with a cleaner, modern aesthetic: deeper blacks, green accent palette, DM Sans + IBM Plex Mono typography, larger border-radii, glassmorphic navigation, refined hover effects, pill badges, and green focus rings. CSS-only change plus Google Fonts. Co-Authored-By: Claude Opus 4.6 * Update src/channels/web/static/style.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/channels/web/static/style.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/channels/web/static/app.js | 232 +++++++++++++++++-- src/channels/web/static/index.html | 51 +++-- src/channels/web/static/style.css | 345 +++++++++++++++++++---------- src/db/libsql/conversations.rs | 13 +- 4 files changed, 487 insertions(+), 154 deletions(-) diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index bdf8b454..57ba3947 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -1204,15 +1204,18 @@ function loadServerLogLevel() { function loadExtensions() { const extList = document.getElementById('extensions-list'); + const wasmList = document.getElementById('available-wasm-list'); + const mcpList = document.getElementById('mcp-servers-list'); const toolsTbody = document.getElementById('tools-tbody'); const toolsEmpty = document.getElementById('tools-empty'); - // Fetch both in parallel + // Fetch all three in parallel Promise.all([ apiFetch('/api/extensions').catch(() => ({ extensions: [] })), apiFetch('/api/extensions/tools').catch(() => ({ tools: [] })), - ]).then(([extData, toolData]) => { - // Render extensions + apiFetch('/api/extensions/registry').catch(function(err) { console.warn('registry fetch failed:', err); return { entries: [] }; }), + ]).then(([extData, toolData, registryData]) => { + // Render installed extensions if (extData.extensions.length === 0) { extList.innerHTML = '
No extensions installed
'; } else { @@ -1222,6 +1225,31 @@ function loadExtensions() { } } + // Split registry entries by kind + var wasmEntries = registryData.entries.filter(function(e) { return e.kind !== 'mcp_server' && !e.installed; }); + var mcpEntries = registryData.entries.filter(function(e) { return e.kind === 'mcp_server'; }); + + // Available WASM extensions + if (wasmEntries.length === 0) { + wasmList.innerHTML = '
No additional WASM extensions available
'; + } else { + wasmList.innerHTML = ''; + for (const entry of wasmEntries) { + wasmList.appendChild(renderAvailableExtensionCard(entry)); + } + } + + // MCP servers (show both installed and uninstalled) + if (mcpEntries.length === 0) { + mcpList.innerHTML = '
No MCP servers available
'; + } else { + mcpList.innerHTML = ''; + for (const entry of mcpEntries) { + var installedExt = extData.extensions.find(function(e) { return e.name === entry.name; }); + mcpList.appendChild(renderMcpServerCard(entry, installedExt)); + } + } + // Render tools if (toolData.tools.length === 0) { toolsTbody.innerHTML = ''; @@ -1235,6 +1263,148 @@ function loadExtensions() { }); } +function renderAvailableExtensionCard(entry) { + const card = document.createElement('div'); + card.className = 'ext-card ext-available'; + + const header = document.createElement('div'); + header.className = 'ext-header'; + + const name = document.createElement('span'); + name.className = 'ext-name'; + name.textContent = entry.display_name; + header.appendChild(name); + + const kind = document.createElement('span'); + kind.className = 'ext-kind kind-' + entry.kind; + kind.textContent = entry.kind; + header.appendChild(kind); + + card.appendChild(header); + + const desc = document.createElement('div'); + desc.className = 'ext-desc'; + desc.textContent = entry.description; + card.appendChild(desc); + + if (entry.keywords && entry.keywords.length > 0) { + const kw = document.createElement('div'); + kw.className = 'ext-keywords'; + kw.textContent = entry.keywords.join(', '); + card.appendChild(kw); + } + + const actions = document.createElement('div'); + actions.className = 'ext-actions'; + + const installBtn = document.createElement('button'); + installBtn.className = 'btn-ext install'; + installBtn.textContent = 'Install'; + installBtn.addEventListener('click', function() { + installBtn.disabled = true; + installBtn.textContent = 'Installing...'; + apiFetch('/api/extensions/install', { + method: 'POST', + body: { name: entry.name, kind: entry.kind }, + }).then(function(res) { + if (res.success) { + showToast('Installed ' + entry.display_name, 'success'); + } else { + showToast('Install: ' + (res.message || 'unknown error'), 'error'); + } + loadExtensions(); + }).catch(function(err) { + showToast('Install failed: ' + err.message, 'error'); + loadExtensions(); + }); + }); + actions.appendChild(installBtn); + + card.appendChild(actions); + return card; +} + +function renderMcpServerCard(entry, installedExt) { + var card = document.createElement('div'); + card.className = 'ext-card' + (installedExt ? '' : ' ext-available'); + + var header = document.createElement('div'); + header.className = 'ext-header'; + + var name = document.createElement('span'); + name.className = 'ext-name'; + name.textContent = entry.display_name; + header.appendChild(name); + + var kind = document.createElement('span'); + kind.className = 'ext-kind kind-mcp_server'; + kind.textContent = 'mcp_server'; + header.appendChild(kind); + + if (installedExt) { + var authDot = document.createElement('span'); + authDot.className = 'ext-auth-dot ' + (installedExt.authenticated ? 'authed' : 'unauthed'); + authDot.title = installedExt.authenticated ? 'Authenticated' : 'Not authenticated'; + header.appendChild(authDot); + } + + card.appendChild(header); + + var desc = document.createElement('div'); + desc.className = 'ext-desc'; + desc.textContent = entry.description; + card.appendChild(desc); + + var actions = document.createElement('div'); + actions.className = 'ext-actions'; + + if (installedExt) { + if (!installedExt.active) { + var activateBtn = document.createElement('button'); + activateBtn.className = 'btn-ext activate'; + activateBtn.textContent = 'Activate'; + activateBtn.addEventListener('click', function() { activateExtension(installedExt.name); }); + actions.appendChild(activateBtn); + } else { + var activeLabel = document.createElement('span'); + activeLabel.className = 'ext-active-label'; + activeLabel.textContent = 'Active'; + actions.appendChild(activeLabel); + } + var removeBtn = document.createElement('button'); + removeBtn.className = 'btn-ext remove'; + removeBtn.textContent = 'Remove'; + removeBtn.addEventListener('click', function() { removeExtension(installedExt.name); }); + actions.appendChild(removeBtn); + } else { + var installBtn = document.createElement('button'); + installBtn.className = 'btn-ext install'; + installBtn.textContent = 'Install'; + installBtn.addEventListener('click', function() { + installBtn.disabled = true; + installBtn.textContent = 'Installing...'; + apiFetch('/api/extensions/install', { + method: 'POST', + body: { name: entry.name, kind: entry.kind }, + }).then(function(res) { + if (res.success) { + showToast('Installed ' + entry.display_name, 'success'); + } else { + showToast('Install: ' + (res.message || 'unknown error'), 'error'); + } + loadExtensions(); + }).catch(function(err) { + showToast('Install failed: ' + err.message, 'error'); + loadExtensions(); + }); + }); + actions.appendChild(installBtn); + } + + card.appendChild(actions); + return card; +} + function renderExtensionCard(ext) { const card = document.createElement('div'); card.className = 'ext-card'; @@ -2195,32 +2365,64 @@ document.getElementById('tee-shield').addEventListener('mouseleave', function() // --- Extension install --- -function installExtension() { - const name = document.getElementById('ext-install-name').value.trim(); +function installWasmExtension() { + var name = document.getElementById('wasm-install-name').value.trim(); if (!name) { showToast('Extension name is required', 'error'); return; } - const url = document.getElementById('ext-install-url').value.trim(); - const kind = document.getElementById('ext-install-kind').value; + var url = document.getElementById('wasm-install-url').value.trim(); + if (!url) { + showToast('URL to .tar.gz bundle is required', 'error'); + return; + } apiFetch('/api/extensions/install', { method: 'POST', - body: { name, url: url || undefined, kind }, - }).then((res) => { + body: { name: name, url: url, kind: 'wasm_tool' }, + }).then(function(res) { if (res.success) { showToast('Installed ' + name, 'success'); - document.getElementById('ext-install-name').value = ''; - document.getElementById('ext-install-url').value = ''; + document.getElementById('wasm-install-name').value = ''; + document.getElementById('wasm-install-url').value = ''; loadExtensions(); } else { showToast('Install failed: ' + (res.message || 'unknown error'), 'error'); } - }).catch((err) => { + }).catch(function(err) { showToast('Install failed: ' + err.message, 'error'); }); } +function addMcpServer() { + var name = document.getElementById('mcp-install-name').value.trim(); + if (!name) { + showToast('Server name is required', 'error'); + return; + } + var url = document.getElementById('mcp-install-url').value.trim(); + if (!url) { + showToast('MCP server URL is required', 'error'); + return; + } + + apiFetch('/api/extensions/install', { + method: 'POST', + body: { name: name, url: url, kind: 'mcp_server' }, + }).then(function(res) { + if (res.success) { + showToast('Added MCP server ' + name, 'success'); + document.getElementById('mcp-install-name').value = ''; + document.getElementById('mcp-install-url').value = ''; + loadExtensions(); + } else { + showToast('Failed to add MCP server: ' + (res.message || 'unknown error'), 'error'); + } + }).catch(function(err) { + showToast('Failed to add MCP server: ' + err.message, 'error'); + }); +} + // --- Keyboard shortcuts --- document.addEventListener('keydown', (e) => { @@ -2228,10 +2430,10 @@ document.addEventListener('keydown', (e) => { const tag = (e.target.tagName || '').toLowerCase(); const inInput = tag === 'input' || tag === 'textarea'; - // Mod+1-6: switch tabs - if (mod && e.key >= '1' && e.key <= '6') { + // Mod+1-5: switch tabs + if (mod && e.key >= '1' && e.key <= '5') { e.preventDefault(); - const tabs = ['chat', 'memory', 'jobs', 'routines', 'logs', 'extensions']; + const tabs = ['chat', 'memory', 'jobs', 'routines', 'extensions']; const idx = parseInt(e.key) - 1; if (tabs[idx]) switchTab(tabs[idx]); return; diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html index d7f65025..0c13974d 100644 --- a/src/channels/web/static/index.html +++ b/src/channels/web/static/index.html @@ -4,6 +4,9 @@ IronClaw + + +