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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* Update src/channels/web/static/style.css

Co-authored-by: Copilot <[email protected]>

* Update src/channels/web/static/style.css

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-21 02:50:51 +00:00
committed by GitHub
co-authored by Claude Opus 4.6 Copilot
parent c038c7705b
commit 250551799b
4 changed files with 487 additions and 154 deletions
+217 -15
View File
@@ -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 = '<div class="empty-state">No extensions installed</div>';
} 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 = '<div class="empty-state">No additional WASM extensions available</div>';
} 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 = '<div class="empty-state">No MCP servers available</div>';
} 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;
+31 -20
View File
@@ -4,6 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IronClaw</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/style.css">
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"
@@ -36,10 +39,10 @@
<button class="active" data-tab="chat">Chat</button>
<button data-tab="memory">Memory</button>
<button data-tab="jobs">Jobs</button>
<button data-tab="logs">Logs</button>
<button data-tab="routines">Routines</button>
<button data-tab="extensions">Extensions</button>
<div class="spacer"></div>
<button class="status-logs-btn" data-tab="logs" title="Logs">Logs</button>
<div class="tee-shield" id="tee-shield" style="display:none" title="Running in a Trusted Execution Environment">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
@@ -185,34 +188,42 @@
<!-- Extensions Tab -->
<div class="tab-panel" id="tab-extensions">
<div class="extensions-container">
<div class="extensions-section">
<h3>Install Extension</h3>
<div class="ext-install-form" id="ext-install-form">
<input type="text" id="ext-install-name" placeholder="Extension name (required)">
<input type="text" id="ext-install-url" placeholder="URL (optional)">
<select id="ext-install-kind">
<option value="mcp_server">MCP Server</option>
<option value="wasm_tool">WASM Tool</option>
<option value="wasm_channel">WASM Channel</option>
</select>
<button onclick="installExtension()">Install</button>
</div>
</div>
<div class="extensions-section">
<h3>Installed Extensions</h3>
<div class="extensions-list" id="extensions-list">
<div class="empty-state">Loading extensions...</div>
</div>
</div>
<div class="extensions-section" id="available-wasm-section">
<h3>Available WASM Extensions</h3>
<div class="extensions-list" id="available-wasm-list">
<div class="empty-state">Loading...</div>
</div>
</div>
<div class="extensions-section">
<h3>Install WASM Extension</h3>
<div class="ext-install-form">
<input type="text" id="wasm-install-name" placeholder="Extension name">
<input type="text" id="wasm-install-url" placeholder="URL to .tar.gz bundle">
<button onclick="installWasmExtension()">Install</button>
</div>
</div>
<div class="extensions-section">
<h3>MCP Servers</h3>
<div class="extensions-list" id="mcp-servers-list">
<div class="empty-state">Loading...</div>
</div>
<h4>Add Custom MCP Server</h4>
<div class="ext-install-form">
<input type="text" id="mcp-install-name" placeholder="Server name">
<input type="text" id="mcp-install-url" placeholder="MCP server URL (https://...)">
<button onclick="addMcpServer()">Add</button>
</div>
</div>
<div class="extensions-section">
<h3>Registered Tools</h3>
<table class="tools-table" id="tools-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<thead><tr><th>Name</th><th>Description</th></tr></thead>
<tbody id="tools-tbody"></tbody>
</table>
<div class="empty-state" id="tools-empty" style="display:none">No tools registered</div>
+232 -113
View File
@@ -1,20 +1,22 @@
/* IronClaw Web Gateway */
:root {
--bg: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border: #30363d;
--text: #e6edf3;
--text-secondary: #8b949e;
--accent: #58a6ff;
--accent-hover: #79c0ff;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--code-bg: #1b2028;
--radius: 6px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--bg: #09090b;
--bg-secondary: #0f0f11;
--bg-tertiary: #1a1a1e;
--border: rgba(255, 255, 255, 0.08);
--text: #fafafa;
--text-secondary: #a1a1aa;
--accent: #34d399;
--accent-hover: #2fc48d;
--success: #34d399;
--warning: #F5A623;
--danger: #E64C4C;
--code-bg: #111113;
--radius: 8px;
--radius-lg: 12px;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
--font-mono: 'IBM Plex Mono', 'SF Mono', 'Fira Code', Consolas, monospace;
}
* {
@@ -24,7 +26,7 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
height: 100vh;
@@ -44,14 +46,14 @@ body {
.auth-card-login {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
border-radius: 16px;
padding: 40px 36px 32px;
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 24px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
}
.auth-brand {
@@ -95,22 +97,29 @@ body {
#auth-screen input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.3);
}
#auth-screen button {
padding: 10px 16px;
background: var(--accent);
color: #fff;
color: #09090b;
border: none;
border-radius: var(--radius);
cursor: pointer;
font-size: 14px;
font-weight: 500;
font-weight: 600;
margin-top: 4px;
transition: background 0.2s, transform 0.2s;
}
#auth-screen button:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
#auth-screen button:active {
transform: scale(0.98);
}
#auth-error {
@@ -137,14 +146,17 @@ body {
/* Tab Bar */
.tab-bar {
display: flex;
background: var(--bg-secondary);
background: rgba(9, 9, 11, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
will-change: backdrop-filter;
border-bottom: 1px solid var(--border);
padding: 0 16px;
gap: 0;
flex-shrink: 0;
}
.tab-bar button {
.tab-bar button:not(.status-logs-btn) {
padding: 10px 20px;
background: none;
border: none;
@@ -152,14 +164,15 @@ body {
color: var(--text-secondary);
cursor: pointer;
font-size: 14px;
transition: color 0.15s, border-color 0.15s;
font-weight: 500;
transition: color 0.2s, border-color 0.2s;
}
.tab-bar button:hover {
.tab-bar button:not(.status-logs-btn):hover {
color: var(--text);
}
.tab-bar button.active {
.tab-bar button:not(.status-logs-btn).active {
color: var(--accent);
border-bottom-color: var(--accent);
}
@@ -168,6 +181,30 @@ body {
flex: 1;
}
.tab-bar .status-logs-btn {
padding: 4px 10px;
background: none;
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-secondary);
cursor: pointer;
font-size: 11px;
align-self: center;
margin-right: 8px;
transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.tab-bar .status-logs-btn:hover {
color: var(--text);
border-color: var(--text-secondary);
}
.tab-bar .status-logs-btn.active {
color: var(--accent);
border-color: var(--accent);
background: rgba(52, 211, 153, 0.1);
}
.tab-bar .status {
display: flex;
align-items: center;
@@ -198,16 +235,16 @@ body {
color: var(--success);
padding: 4px 10px;
border-radius: 12px;
background: rgba(63, 185, 80, 0.1);
border: 1px solid rgba(63, 185, 80, 0.25);
background: rgba(52, 211, 153, 0.1);
border: 1px solid rgba(52, 211, 153, 0.25);
cursor: pointer;
position: relative;
margin-right: 8px;
transition: background 0.15s;
transition: background 0.2s;
}
.tee-shield:hover {
background: rgba(63, 185, 80, 0.18);
background: rgba(52, 211, 153, 0.18);
}
.tee-shield svg {
@@ -225,9 +262,11 @@ body {
top: 100%;
right: 0;
margin-top: 8px;
background: var(--bg-secondary);
background: rgba(15, 15, 17, 0.9);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
padding: 16px;
min-width: 340px;
max-width: 420px;
@@ -272,12 +311,12 @@ body {
.tee-field-value {
font-size: 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
color: var(--text);
word-break: break-all;
background: var(--bg);
padding: 4px 8px;
border-radius: 4px;
border-radius: var(--radius);
border: 1px solid var(--border);
}
@@ -295,7 +334,7 @@ body {
color: var(--text-secondary);
cursor: pointer;
font-size: 11px;
transition: color 0.15s, border-color 0.15s;
transition: color 0.2s, border-color 0.2s;
}
.tee-btn-copy:hover {
@@ -350,7 +389,7 @@ body {
.message.user {
align-self: flex-end;
background: var(--accent);
color: #fff;
color: #09090b;
border-bottom-right-radius: 2px;
white-space: pre-wrap;
}
@@ -469,11 +508,12 @@ body {
max-width: 80%;
background: var(--bg-secondary);
border: 1px solid var(--warning);
border-radius: var(--radius);
border-radius: var(--radius-lg);
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
transition: border-color 0.2s;
}
.approval-header {
@@ -488,7 +528,7 @@ body {
font-size: 14px;
font-weight: 600;
color: var(--text);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
}
.approval-description {
@@ -516,7 +556,7 @@ body {
padding: 8px 12px;
border-radius: var(--radius);
font-size: 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
line-height: 1.4;
overflow-x: auto;
color: var(--text-secondary);
@@ -549,13 +589,15 @@ body {
.approval-card .approval-actions button.approve {
background: var(--success);
border-color: var(--success);
color: #fff;
color: #09090b;
font-weight: 600;
}
.approval-card .approval-actions button.always {
background: var(--accent);
border-color: var(--accent);
color: #fff;
color: #09090b;
font-weight: 600;
}
.approval-card .approval-actions button.deny {
@@ -577,12 +619,13 @@ body {
max-width: 80%;
background: var(--bg-secondary);
border: 1px solid var(--accent);
border-radius: var(--radius);
border-radius: var(--radius-lg);
padding: 12px 16px;
margin: 8px 0;
display: flex;
flex-direction: column;
gap: 8px;
transition: border-color 0.2s;
}
.auth-card .auth-header {
@@ -623,12 +666,13 @@ body {
background: var(--bg);
color: var(--text);
font-size: 13px;
font-family: monospace;
font-family: var(--font-mono);
}
.auth-card .auth-token-input input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.auth-card .auth-actions {
@@ -655,7 +699,8 @@ body {
.auth-card .auth-actions button.auth-submit {
background: var(--accent);
border-color: var(--accent);
color: #fff;
color: #09090b;
font-weight: 600;
}
.auth-card .auth-actions button.auth-cancel {
@@ -666,7 +711,8 @@ body {
.auth-card .auth-actions button.auth-oauth {
background: var(--success);
border-color: var(--success);
color: #fff;
color: #09090b;
font-weight: 600;
}
.auth-card .auth-error {
@@ -700,21 +746,29 @@ body {
.chat-input textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.chat-input button {
padding: 8px 20px;
background: var(--accent);
color: #fff;
color: #09090b;
border: none;
border-radius: var(--radius);
cursor: pointer;
font-size: 14px;
font-weight: 600;
align-self: flex-end;
transition: background 0.2s, transform 0.2s;
}
.chat-input button:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
.chat-input button:active {
transform: scale(0.98);
}
.chat-input button:disabled {
@@ -755,6 +809,7 @@ body {
.memory-sidebar input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.memory-tree {
@@ -888,7 +943,7 @@ body {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
}
.memory-viewer .empty {
@@ -942,8 +997,13 @@ body {
padding: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
text-align: center;
transition: border-color 0.2s, transform 0.2s;
}
.summary-card:hover {
border-color: rgba(255, 255, 255, 0.15);
}
.summary-card .count {
@@ -987,24 +1047,24 @@ body {
}
.jobs-table tr:hover td {
background: var(--bg-secondary);
background: rgba(255, 255, 255, 0.03);
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
padding: 3px 10px;
border-radius: 9999px;
font-size: 11px;
font-weight: 500;
}
.badge.pending { background: var(--bg-tertiary); color: var(--text-secondary); }
.badge.in_progress { background: rgba(88, 166, 255, 0.15); color: var(--accent); }
.badge.completed { background: rgba(63, 185, 80, 0.15); color: var(--success); }
.badge.failed { background: rgba(248, 81, 73, 0.15); color: var(--danger); }
.badge.stuck { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
.badge.in_progress { background: rgba(52, 211, 153, 0.15); color: var(--accent); }
.badge.completed { background: rgba(52, 211, 153, 0.15); color: var(--success); }
.badge.failed { background: rgba(230, 76, 76, 0.15); color: var(--danger); }
.badge.stuck { background: rgba(245, 166, 35, 0.15); color: var(--warning); }
.badge.cancelled { background: var(--bg-tertiary); color: var(--text-secondary); }
.badge.interrupted { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
.badge.interrupted { background: rgba(245, 166, 35, 0.15); color: var(--warning); }
.badge.source-sandbox { background: rgba(136, 132, 216, 0.15); color: #b4b0e8; }
.badge.source-direct { background: var(--bg-tertiary); color: var(--text-secondary); }
@@ -1019,7 +1079,7 @@ body {
}
.btn-cancel:hover {
background: rgba(248, 81, 73, 0.15);
background: rgba(230, 76, 76, 0.15);
}
.btn-restart {
@@ -1033,7 +1093,7 @@ body {
}
.btn-restart:hover {
background: rgba(88, 166, 255, 0.15);
background: rgba(52, 211, 153, 0.15);
}
.btn-browse {
@@ -1048,7 +1108,7 @@ body {
}
.btn-browse:hover {
background: rgba(63, 185, 80, 0.15);
background: rgba(52, 211, 153, 0.15);
}
/* Job started card in chat */
@@ -1060,8 +1120,13 @@ body {
margin: 8px 0;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
border-left: 3px solid var(--accent);
transition: border-color 0.2s, transform 0.2s;
}
.job-card:hover {
border-color: rgba(255, 255, 255, 0.15);
}
.job-card-icon {
@@ -1080,7 +1145,7 @@ body {
.job-card-id {
font-size: 12px;
color: var(--text-secondary);
font-family: monospace;
font-family: var(--font-mono);
}
.job-card-view, .job-card-browse {
@@ -1098,7 +1163,7 @@ body {
}
.job-card-view:hover {
background: rgba(88, 166, 255, 0.15);
background: rgba(52, 211, 153, 0.15);
}
.job-card-browse {
@@ -1108,7 +1173,7 @@ body {
}
.job-card-browse:hover {
background: rgba(63, 185, 80, 0.15);
background: rgba(52, 211, 153, 0.15);
}
/* Clickable job rows */
@@ -1314,7 +1379,7 @@ body {
.action-tool {
font-weight: 600;
color: var(--text);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
}
.action-seq {
@@ -1359,7 +1424,7 @@ body {
padding: 8px 12px;
border-radius: var(--radius);
font-size: 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
line-height: 1.4;
overflow-x: auto;
color: var(--text-secondary);
@@ -1371,11 +1436,11 @@ body {
}
.action-error {
background: rgba(248, 81, 73, 0.1);
background: rgba(230, 76, 76, 0.1);
padding: 8px 12px;
border-radius: var(--radius);
font-size: 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
line-height: 1.4;
color: var(--danger);
margin: 0;
@@ -1413,8 +1478,8 @@ body {
.conv-system .conv-body { color: var(--text-secondary); font-size: 13px; }
.conv-user {
background: rgba(88, 166, 255, 0.08);
border: 1px solid rgba(88, 166, 255, 0.2);
background: rgba(52, 211, 153, 0.08);
border: 1px solid rgba(52, 211, 153, 0.2);
}
.conv-user .conv-role { color: var(--accent); }
@@ -1429,7 +1494,7 @@ body {
.conv-tool {
background: var(--bg-secondary);
border: 1px solid var(--border);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
font-size: 13px;
}
@@ -1440,7 +1505,7 @@ body {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 4px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
}
.conv-tool-calls {
@@ -1457,7 +1522,7 @@ body {
font-size: 12px;
font-weight: 600;
color: var(--accent);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
}
.conv-tc-args {
@@ -1465,7 +1530,7 @@ body {
padding: 6px 10px;
border-radius: var(--radius);
font-size: 11px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
line-height: 1.4;
margin: 4px 0 0;
color: var(--text-secondary);
@@ -1505,12 +1570,12 @@ body {
font-size: 12px;
color: var(--accent);
margin-bottom: 8px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
}
.job-files-content {
font-size: 13px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
@@ -1560,7 +1625,7 @@ body {
}
.routines-table tr:hover td {
background: var(--bg-secondary);
background: rgba(255, 255, 255, 0.03);
}
.routine-row {
@@ -1571,9 +1636,9 @@ body {
padding: 16px 0;
}
.badge.enabled { background: rgba(63, 185, 80, 0.15); color: var(--success); }
.badge.enabled { background: rgba(52, 211, 153, 0.15); color: var(--success); }
.badge.disabled { background: var(--bg-tertiary); color: var(--text-secondary); }
.badge.failing { background: rgba(248, 81, 73, 0.15); color: var(--danger); }
.badge.failing { background: rgba(230, 76, 76, 0.15); color: var(--danger); }
.btn-trigger {
padding: 4px 10px;
@@ -1586,7 +1651,7 @@ body {
}
.btn-trigger:hover {
background: rgba(88, 166, 255, 0.15);
background: rgba(52, 211, 153, 0.15);
}
.btn-toggle {
@@ -1600,7 +1665,7 @@ body {
}
.btn-toggle:hover {
background: rgba(210, 153, 34, 0.15);
background: rgba(245, 166, 35, 0.15);
}
/* Logs Tab */
@@ -1644,6 +1709,7 @@ body {
.logs-toolbar input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.logs-checkbox {
@@ -1673,7 +1739,7 @@ body {
flex: 1;
overflow-y: auto;
padding: 4px 0;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
background: var(--bg);
@@ -1758,6 +1824,13 @@ body {
color: var(--text);
}
.extensions-section h4 {
font-size: 13px;
font-weight: 600;
margin: 16px 0 8px;
color: var(--text-secondary);
}
.extensions-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@@ -1767,11 +1840,16 @@ body {
.ext-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
transition: border-color 0.2s, transform 0.2s;
}
.ext-card:hover {
border-color: rgba(255, 255, 255, 0.15);
}
.ext-header {
@@ -1796,17 +1874,17 @@ body {
}
.ext-kind.kind-mcp_server {
background: rgba(88, 166, 255, 0.15);
background: rgba(52, 211, 153, 0.15);
color: var(--accent);
}
.ext-kind.kind-wasm_tool {
background: rgba(63, 185, 80, 0.15);
background: rgba(52, 211, 153, 0.15);
color: var(--success);
}
.ext-kind.kind-wasm_channel {
background: rgba(210, 153, 34, 0.15);
background: rgba(245, 166, 35, 0.15);
color: var(--warning);
}
@@ -1834,7 +1912,7 @@ body {
.ext-url {
font-size: 12px;
color: var(--text-secondary);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1878,7 +1956,7 @@ body {
}
.btn-ext.activate:hover {
background: rgba(88, 166, 255, 0.15);
background: rgba(52, 211, 153, 0.15);
}
.btn-ext.remove {
@@ -1887,7 +1965,31 @@ body {
}
.btn-ext.remove:hover {
background: rgba(248, 81, 73, 0.15);
background: rgba(230, 76, 76, 0.15);
}
.btn-ext.install {
border-color: var(--success);
color: var(--success);
}
.btn-ext.install:hover {
background: rgba(52, 211, 153, 0.15);
}
.btn-ext.install:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ext-available {
border-style: dashed;
}
.ext-keywords {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
}
.tools-table {
@@ -1912,7 +2014,7 @@ body {
}
.tools-table tr:hover td {
background: var(--bg-secondary);
background: rgba(255, 255, 255, 0.03);
}
/* --- Activity tab (unified sandbox job events) --- */
@@ -1921,12 +2023,12 @@ body {
flex: 1;
overflow-y: auto;
padding: 12px;
font-family: monospace;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
border-radius: var(--radius);
margin-bottom: 8px;
max-height: calc(100vh - 320px);
}
@@ -2020,7 +2122,7 @@ body {
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
border-radius: var(--radius);
color: var(--text);
font-size: 13px;
}
@@ -2028,20 +2130,28 @@ body {
.activity-input-bar input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.activity-input-bar button {
padding: 8px 16px;
background: var(--accent);
color: #fff;
color: #09090b;
border: none;
border-radius: 6px;
border-radius: var(--radius);
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background 0.2s, transform 0.2s;
}
.activity-input-bar button:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
.activity-input-bar button:active {
transform: scale(0.98);
}
#activity-done-btn {
@@ -2131,7 +2241,7 @@ body {
/* --- Memory search highlighting --- */
mark {
background: rgba(88, 166, 255, 0.3);
background: rgba(52, 211, 153, 0.3);
color: inherit;
border-radius: 2px;
padding: 0 1px;
@@ -2197,7 +2307,7 @@ mark {
}
.thread-new-btn:hover {
background: rgba(88, 166, 255, 0.15);
background: rgba(52, 211, 153, 0.15);
}
.assistant-item {
@@ -2218,7 +2328,7 @@ mark {
}
.assistant-item.active {
background: rgba(88, 166, 255, 0.08);
background: rgba(52, 211, 153, 0.08);
color: var(--accent);
border-left: 2px solid var(--accent);
}
@@ -2285,7 +2395,7 @@ mark {
}
.thread-label {
font-family: monospace;
font-family: var(--font-mono);
font-size: 12px;
}
@@ -2332,7 +2442,7 @@ mark {
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.5;
resize: none;
@@ -2341,6 +2451,7 @@ mark {
.memory-editor textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.memory-editor-actions {
@@ -2351,15 +2462,22 @@ mark {
.btn-save {
padding: 6px 16px;
background: var(--accent);
color: #fff;
color: #09090b;
border: none;
border-radius: var(--radius);
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background 0.2s, transform 0.2s;
}
.btn-save:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
.btn-save:active {
transform: scale(0.98);
}
.btn-cancel-edit {
@@ -2425,9 +2543,11 @@ mark {
top: 100%;
right: 0;
margin-top: 8px;
background: var(--bg-secondary);
background: rgba(15, 15, 17, 0.9);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
padding: 12px;
min-width: 180px;
box-shadow: var(--shadow);
@@ -2472,29 +2592,28 @@ mark {
.ext-install-form input:focus {
outline: none;
border-color: var(--accent);
}
.ext-install-form select {
padding: 6px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 13px;
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
.ext-install-form button {
padding: 6px 16px;
background: var(--accent);
color: #fff;
color: #09090b;
border: none;
border-radius: var(--radius);
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background 0.2s, transform 0.2s;
}
.ext-install-form button:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
.ext-install-form button:active {
transform: scale(0.98);
}
/* --- Activity toolbar --- */
@@ -2518,6 +2637,7 @@ mark {
.activity-toolbar select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.1);
}
/* --- Mobile responsive --- */
@@ -2530,7 +2650,7 @@ mark {
padding: 0 8px;
}
.tab-bar button {
.tab-bar button:not(.status-logs-btn) {
padding: 8px 12px;
font-size: 13px;
white-space: nowrap;
@@ -2616,8 +2736,7 @@ mark {
align-items: stretch;
}
.ext-install-form input,
.ext-install-form select {
.ext-install-form input {
width: 100%;
}
}
+7 -6
View File
@@ -49,9 +49,10 @@ impl ConversationStore for LibSqlBackend {
) -> Result<Uuid, DatabaseError> {
let conn = self.connect().await?;
let id = Uuid::new_v4();
let now = fmt_ts(&Utc::now());
conn.execute(
"INSERT INTO conversation_messages (id, conversation_id, role, content) VALUES (?1, ?2, ?3, ?4)",
params![id.to_string(), conversation_id.to_string(), role, content],
"INSERT INTO conversation_messages (id, conversation_id, role, content, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
params![id.to_string(), conversation_id.to_string(), role, content, now],
)
.await
.map_err(|e| DatabaseError::Query(e.to_string()))?;
@@ -100,7 +101,7 @@ impl ConversationStore for LibSqlBackend {
(SELECT substr(m2.content, 1, 100)
FROM conversation_messages m2
WHERE m2.conversation_id = c.id AND m2.role = 'user'
ORDER BY m2.created_at ASC
ORDER BY m2.created_at ASC, m2.rowid ASC
LIMIT 1
) AS title
FROM conversations c
@@ -216,7 +217,7 @@ impl ConversationStore for LibSqlBackend {
SELECT id, role, content, created_at
FROM conversation_messages
WHERE conversation_id = ?1 AND created_at < ?2
ORDER BY created_at DESC
ORDER BY created_at DESC, rowid DESC
LIMIT ?3
"#,
params![cid, fmt_ts(&before_ts), fetch_limit],
@@ -228,7 +229,7 @@ impl ConversationStore for LibSqlBackend {
SELECT id, role, content, created_at
FROM conversation_messages
WHERE conversation_id = ?1
ORDER BY created_at DESC
ORDER BY created_at DESC, rowid DESC
LIMIT ?2
"#,
params![cid, fetch_limit],
@@ -309,7 +310,7 @@ impl ConversationStore for LibSqlBackend {
SELECT id, role, content, created_at
FROM conversation_messages
WHERE conversation_id = ?1
ORDER BY created_at ASC
ORDER BY created_at ASC, rowid ASC
"#,
params![conversation_id.to_string()],
)