diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/app.js | 703 | ||||
| -rw-r--r-- | web/style.css | 536 |
2 files changed, 1234 insertions, 5 deletions
@@ -1,4 +1,5 @@ -const API_BASE = window.location.origin; +const BASE_PATH = document.querySelector('meta[name="base-path"]')?.content ?? ''; +const API_BASE = window.location.origin + BASE_PATH; // ── Fetch ───────────────────────────────────────────────────────────────────── @@ -8,6 +9,12 @@ async function fetchTasks() { return res.json(); } +async function fetchTemplates() { + const res = await fetch(`${API_BASE}/api/templates`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + // ── Render ──────────────────────────────────────────────────────────────────── function formatDate(iso) { @@ -70,15 +77,48 @@ function createTaskCard(task) { const btn = document.createElement('button'); btn.className = 'btn-run'; btn.textContent = 'Run'; - btn.addEventListener('click', () => handleRun(task.id, btn, footer)); + btn.addEventListener('click', (e) => { + e.stopPropagation(); + handleRun(task.id, btn, footer); + }); footer.appendChild(btn); card.appendChild(footer); } + card.addEventListener('click', () => openTaskPanel(task.id)); + return card; } +// ── Filter ──────────────────────────────────────────────────────────────────── + +const HIDE_STATES = new Set(['COMPLETED', 'FAILED']); + +let showHiddenFold = false; + +function filterTasks(tasks, hideCompletedFailed = false) { + if (!hideCompletedFailed) return tasks; + return tasks.filter(t => !HIDE_STATES.has(t.state)); +} + +function getHideCompletedFailed() { + const stored = localStorage.getItem('hideCompletedFailed'); + return stored === null ? true : stored === 'true'; +} + +function setHideCompletedFailed(val) { + localStorage.setItem('hideCompletedFailed', String(val)); +} + +function updateToggleButton() { + const btn = document.getElementById('btn-toggle-completed'); + if (!btn) return; + btn.textContent = getHideCompletedFailed() + ? 'Show completed/failed' + : 'Hide completed/failed'; +} + function renderTaskList(tasks) { const container = document.querySelector('.task-list'); @@ -87,11 +127,93 @@ function renderTaskList(tasks) { return; } + const hide = getHideCompletedFailed(); + const visible = filterTasks(tasks, hide); + const hiddenCount = tasks.length - visible.length; + // Replace contents with task cards container.innerHTML = ''; - for (const task of tasks) { + for (const task of visible) { container.appendChild(createTaskCard(task)); } + + if (hiddenCount > 0) { + const info = document.createElement('button'); + info.className = 'hidden-tasks-info'; + const arrow = showHiddenFold ? '▼' : '▶'; + info.textContent = `${arrow} ${hiddenCount} hidden task${hiddenCount === 1 ? '' : 's'}`; + info.addEventListener('click', () => { + showHiddenFold = !showHiddenFold; + renderTaskList(tasks); + }); + container.appendChild(info); + + if (showHiddenFold) { + const fold = document.createElement('div'); + fold.className = 'hidden-tasks-fold'; + const hiddenTasks = tasks.filter(t => HIDE_STATES.has(t.state)); + for (const task of hiddenTasks) { + fold.appendChild(createTaskCard(task)); + } + container.appendChild(fold); + } + } +} + +function createTemplateCard(tmpl) { + const card = document.createElement('div'); + card.className = 'template-card'; + + const name = document.createElement('div'); + name.className = 'template-name'; + name.textContent = tmpl.name; + card.appendChild(name); + + if (tmpl.description) { + const desc = document.createElement('div'); + desc.className = 'template-description'; + desc.textContent = tmpl.description; + card.appendChild(desc); + } + + if (tmpl.tags && tmpl.tags.length > 0) { + const tagsEl = document.createElement('div'); + tagsEl.className = 'template-tags'; + for (const tag of tmpl.tags) { + const chip = document.createElement('span'); + chip.className = 'tag-chip'; + chip.textContent = tag; + tagsEl.appendChild(chip); + } + card.appendChild(tagsEl); + } + + const footer = document.createElement('div'); + footer.className = 'template-card-footer'; + + const delBtn = document.createElement('button'); + delBtn.className = 'btn-danger btn-sm'; + delBtn.textContent = 'Delete'; + delBtn.addEventListener('click', () => deleteTemplate(tmpl.id)); + + footer.appendChild(delBtn); + card.appendChild(footer); + + return card; +} + +function renderTemplateList(templates) { + const container = document.querySelector('.template-list'); + + if (!templates || templates.length === 0) { + container.innerHTML = '<div id="loading">No templates yet.</div>'; + return; + } + + container.innerHTML = ''; + for (const tmpl of templates) { + container.appendChild(createTemplateCard(tmpl)); + } } // ── Run action ──────────────────────────────────────────────────────────────── @@ -130,6 +252,23 @@ async function handleRun(taskId, btn, footer) { } } +// ── Delete template ──────────────────────────────────────────────────────────── + +async function deleteTemplate(id) { + if (!window.confirm('Delete this template?')) return; + + const res = await fetch(`${API_BASE}/api/templates/${id}`, { method: 'DELETE' }); + if (!res.ok) { + let msg = `HTTP ${res.status}`; + try { const body = await res.json(); msg = body.error || body.message || msg; } catch {} + alert(`Failed to delete: ${msg}`); + return; + } + + const templates = await fetchTemplates(); + renderTemplateList(templates); +} + // ── Polling ─────────────────────────────────────────────────────────────────── async function poll() { @@ -147,6 +286,562 @@ function startPolling(intervalMs = 10_000) { setInterval(poll, intervalMs); } +// ── Elaborate (Draft with AI) ───────────────────────────────────────────────── + +async function elaborateTask(prompt) { + const res = await fetch(`${API_BASE}/api/tasks/elaborate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt }), + }); + if (!res.ok) { + let msg = `HTTP ${res.status}`; + try { const body = await res.json(); msg = body.error || msg; } catch {} + throw new Error(msg); + } + return res.json(); +} + +// ── Task modal ──────────────────────────────────────────────────────────────── + +function openTaskModal() { + document.getElementById('task-modal').showModal(); +} + +function closeTaskModal() { + document.getElementById('task-modal').close(); + document.getElementById('task-form').reset(); + document.getElementById('elaborate-prompt').value = ''; +} + +async function createTask(formData) { + const body = { + name: formData.get('name'), + description: '', + claude: { + model: formData.get('model'), + instructions: formData.get('instructions'), + working_dir: formData.get('working_dir'), + max_budget_usd: parseFloat(formData.get('max_budget_usd')), + }, + timeout: formData.get('timeout'), + priority: formData.get('priority'), + tags: [], + }; + + const res = await fetch(`${API_BASE}/api/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `HTTP ${res.status}`); + } + + closeTaskModal(); + const tasks = await fetchTasks(); + renderTaskList(tasks); +} + +// ── Template modal ──────────────────────────────────────────────────────────── + +function openTemplateModal() { + document.getElementById('template-modal').showModal(); +} + +function closeTemplateModal() { + document.getElementById('template-modal').close(); + document.getElementById('template-form').reset(); +} + +async function saveTemplate(formData) { + const splitTrim = val => val.split(',').map(s => s.trim()).filter(Boolean); + + const body = { + name: formData.get('name'), + description: formData.get('description'), + claude: { + model: formData.get('model'), + instructions: formData.get('instructions'), + working_dir: formData.get('working_dir'), + max_budget_usd: parseFloat(formData.get('max_budget_usd')), + allowed_tools: splitTrim(formData.get('allowed_tools') || ''), + }, + timeout: formData.get('timeout'), + priority: formData.get('priority'), + tags: splitTrim(formData.get('tags') || ''), + }; + + const res = await fetch(`${API_BASE}/api/templates`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `HTTP ${res.status}`); + } + + closeTemplateModal(); + const templates = await fetchTemplates(); + renderTemplateList(templates); +} + +// ── Task side panel ─────────────────────────────────────────────────────────── + +// Format Go's task.Duration JSON value {"Duration": <nanoseconds>} to human string. +function formatDurationNs(timeout) { + const ns = timeout && timeout.Duration; + if (!ns) return '—'; + const secs = ns / 1e9; + if (secs < 60) return `${secs.toFixed(1)}s`; + const mins = Math.floor(secs / 60); + const remSecs = Math.floor(secs % 60); + if (mins < 60) return remSecs > 0 ? `${mins}m ${remSecs}s` : `${mins}m`; + const hrs = Math.floor(mins / 60); + const remMins = mins % 60; + return remMins > 0 ? `${hrs}h ${remMins}m` : `${hrs}h`; +} + +function formatDateLong(iso) { + if (!iso) return '—'; + return new Date(iso).toLocaleString(undefined, { + year: 'numeric', month: 'short', day: 'numeric', + hour: '2-digit', minute: '2-digit', second: '2-digit', + }); +} + +function openTaskPanel(taskId) { + const panel = document.getElementById('task-panel'); + const backdrop = document.getElementById('task-panel-backdrop'); + const content = document.getElementById('task-panel-content'); + document.getElementById('task-panel-title').textContent = 'Task Details'; + + content.innerHTML = ''; + const loading = document.createElement('div'); + loading.className = 'panel-loading'; + loading.textContent = 'Loading…'; + content.appendChild(loading); + + backdrop.hidden = false; + panel.classList.add('open'); + + Promise.all([ + fetch(`${API_BASE}/api/tasks/${taskId}`).then(r => { + if (!r.ok) throw new Error(`HTTP ${r.status}`); + return r.json(); + }), + fetch(`${API_BASE}/api/tasks/${taskId}/executions`).then(r => { + if (!r.ok) throw new Error(`HTTP ${r.status}`); + return r.json(); + }), + ]).then(([task, executions]) => { + renderTaskPanel(task, executions); + }).catch(err => { + content.innerHTML = ''; + const errEl = document.createElement('div'); + errEl.className = 'panel-fetch-error'; + errEl.textContent = `Failed to load: ${err.message}`; + content.appendChild(errEl); + }); +} + +function closeTaskPanel() { + document.getElementById('task-panel').classList.remove('open'); + document.getElementById('task-panel-backdrop').hidden = true; +} + +function makeSection(title) { + const section = document.createElement('div'); + section.className = 'panel-section'; + const hdr = document.createElement('div'); + hdr.className = 'panel-section-title'; + hdr.textContent = title; + section.appendChild(hdr); + return section; +} + +function makeMetaItem(label, valueText, opts = {}) { + const item = document.createElement('div'); + item.className = 'meta-item' + (opts.fullWidth ? ' full-width' : ''); + + const lbl = document.createElement('div'); + lbl.className = 'meta-label'; + lbl.textContent = label; + item.appendChild(lbl); + + if (opts.badge) { + const badge = document.createElement('span'); + badge.className = 'state-badge'; + badge.dataset.state = valueText; + badge.textContent = valueText.replace(/_/g, ' '); + item.appendChild(badge); + } else if (opts.code) { + const pre = document.createElement('pre'); + pre.className = 'panel-code'; + pre.textContent = valueText; + item.appendChild(pre); + } else if (opts.tags) { + const wrap = document.createElement('div'); + if (opts.tags.length > 0) { + wrap.className = 'panel-tags'; + for (const tag of opts.tags) { + const chip = document.createElement('span'); + chip.className = 'tag-chip'; + chip.textContent = tag; + wrap.appendChild(chip); + } + } else { + wrap.className = 'meta-value muted'; + wrap.textContent = '—'; + } + item.appendChild(wrap); + } else { + const val = document.createElement('div'); + val.className = 'meta-value' + (opts.mono ? ' mono' : '') + (opts.muted ? ' muted' : ''); + val.textContent = valueText || '—'; + item.appendChild(val); + } + return item; +} + +function renderTaskPanel(task, executions) { + document.getElementById('task-panel-title').textContent = task.name; + const content = document.getElementById('task-panel-content'); + content.innerHTML = ''; + + // ── Overview ── + const overview = makeSection('Overview'); + const overviewGrid = document.createElement('div'); + overviewGrid.className = 'meta-grid'; + overviewGrid.append( + makeMetaItem('State', task.state, { badge: true }), + makeMetaItem('Priority', task.priority), + makeMetaItem('Created', formatDateLong(task.created_at)), + makeMetaItem('Updated', formatDateLong(task.updated_at)), + makeMetaItem('ID', task.id, { fullWidth: true, mono: true }), + ); + if (task.parent_task_id) { + overviewGrid.append(makeMetaItem('Parent Task', task.parent_task_id, { fullWidth: true, mono: true })); + } + if (task.tags && task.tags.length >= 0) { + overviewGrid.append(makeMetaItem('Tags', '', { fullWidth: true, tags: task.tags || [] })); + } + if (task.description) { + overviewGrid.append(makeMetaItem('Description', task.description, { fullWidth: true })); + } + overview.appendChild(overviewGrid); + content.appendChild(overview); + + // ── Claude Config ── + const c = task.claude || {}; + const claudeSection = makeSection('Claude Config'); + const claudeGrid = document.createElement('div'); + claudeGrid.className = 'meta-grid'; + claudeGrid.append( + makeMetaItem('Model', c.model), + makeMetaItem('Max Budget', c.max_budget_usd != null ? `$${c.max_budget_usd.toFixed(2)}` : '—'), + makeMetaItem('Working Dir', c.working_dir), + makeMetaItem('Permission Mode', c.permission_mode || 'default'), + ); + if (c.allowed_tools && c.allowed_tools.length > 0) { + claudeGrid.append(makeMetaItem('Allowed Tools', c.allowed_tools.join(', '), { fullWidth: true })); + } + if (c.disallowed_tools && c.disallowed_tools.length > 0) { + claudeGrid.append(makeMetaItem('Disallowed Tools', c.disallowed_tools.join(', '), { fullWidth: true })); + } + if (c.instructions) { + claudeGrid.append(makeMetaItem('Instructions', c.instructions, { fullWidth: true, code: true })); + } + if (c.system_prompt_append) { + claudeGrid.append(makeMetaItem('System Prompt Append', c.system_prompt_append, { fullWidth: true, code: true })); + } + claudeSection.appendChild(claudeGrid); + content.appendChild(claudeSection); + + // ── Execution Settings ── + const settingsSection = makeSection('Execution Settings'); + const settingsGrid = document.createElement('div'); + settingsGrid.className = 'meta-grid'; + settingsGrid.append( + makeMetaItem('Timeout', formatDurationNs(task.timeout)), + makeMetaItem('Retry Attempts', String(task.retry ? task.retry.max_attempts : 1)), + makeMetaItem('Backoff', task.retry ? task.retry.backoff : '—'), + ); + if (task.depends_on && task.depends_on.length > 0) { + settingsGrid.append(makeMetaItem('Depends On', task.depends_on.join(', '), { fullWidth: true, mono: true })); + } + settingsSection.appendChild(settingsGrid); + content.appendChild(settingsSection); + + // ── Executions ── + const execSection = makeSection('Executions'); + if (!executions || executions.length === 0) { + const none = document.createElement('div'); + none.className = 'meta-value muted'; + none.textContent = 'No executions yet.'; + execSection.appendChild(none); + } else { + const list = document.createElement('div'); + list.className = 'executions-list'; + // Newest first + for (const exec of [...executions].reverse()) { + const row = document.createElement('div'); + row.className = 'execution-row'; + + const shortId = document.createElement('span'); + shortId.className = 'execution-id'; + shortId.textContent = exec.ID ? exec.ID.slice(0, 8) : '—'; + row.appendChild(shortId); + + const badge = document.createElement('span'); + badge.className = 'state-badge'; + badge.dataset.state = exec.Status || ''; + badge.textContent = (exec.Status || '—').replace(/_/g, ' '); + row.appendChild(badge); + + const times = document.createElement('span'); + times.className = 'execution-times'; + const start = exec.StartTime ? formatDate(exec.StartTime) : '?'; + const end = exec.EndTime && exec.EndTime !== '0001-01-01T00:00:00Z' ? formatDate(exec.EndTime) : '…'; + times.textContent = `${start} → ${end}`; + row.appendChild(times); + + if (exec.CostUSD != null && exec.CostUSD > 0) { + const cost = document.createElement('span'); + cost.className = 'execution-cost'; + cost.textContent = `$${exec.CostUSD.toFixed(4)}`; + row.appendChild(cost); + } + + const exitEl = document.createElement('span'); + exitEl.className = 'execution-exit'; + exitEl.textContent = `exit: ${exec.ExitCode ?? '—'}`; + row.appendChild(exitEl); + + const logsBtn = document.createElement('button'); + logsBtn.className = 'btn-view-logs'; + logsBtn.textContent = 'View Logs'; + logsBtn.addEventListener('click', () => handleViewLogs(exec.ID)); + row.appendChild(logsBtn); + + list.appendChild(row); + } + execSection.appendChild(list); + } + content.appendChild(execSection); +} + +async function handleViewLogs(execId) { + const modal = document.getElementById('logs-modal'); + const body = document.getElementById('logs-modal-body'); + document.getElementById('logs-modal-title').textContent = `Execution ${execId.slice(0, 8)}`; + body.innerHTML = '<div class="panel-loading">Loading…</div>'; + modal.showModal(); + + try { + const res = await fetch(`${API_BASE}/api/executions/${execId}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const exec = await res.json(); + + body.innerHTML = ''; + const grid = document.createElement('div'); + grid.className = 'meta-grid'; + + const entries = [ + ['ID', exec.ID, { fullWidth: true, mono: true }], + ['Status', exec.Status, { badge: true }], + ['Exit Code', String(exec.ExitCode ?? '—'), {}], + ['Cost', exec.CostUSD > 0 ? `$${exec.CostUSD.toFixed(4)}` : '—', {}], + ['Start', formatDateLong(exec.StartTime), {}], + ['End', exec.EndTime && !exec.EndTime.startsWith('0001-') ? formatDateLong(exec.EndTime) : '—', {}], + ['Error', exec.ErrorMsg || '—', { fullWidth: true }], + ['Stdout', exec.StdoutPath || '—', { fullWidth: true, mono: true }], + ['Stderr', exec.StderrPath || '—', { fullWidth: true, mono: true }], + ]; + for (const [label, value, opts] of entries) { + grid.appendChild(makeMetaItem(label, value, opts)); + } + body.appendChild(grid); + } catch (err) { + body.innerHTML = `<div class="panel-fetch-error">Failed to load: ${err.message}</div>`; + } +} + +// ── Tab switching ───────────────────────────────────────────────────────────── + +function switchTab(name) { + // Update tab button active state + document.querySelectorAll('.tab').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tab === name); + }); + + // Show/hide panels + document.querySelectorAll('[data-panel]').forEach(panel => { + if (panel.dataset.panel === name) { + panel.removeAttribute('hidden'); + } else { + panel.setAttribute('hidden', ''); + } + }); + + // Show/hide the header New Task button (only relevant on tasks tab) + document.getElementById('btn-new-task').style.display = + name === 'tasks' ? '' : 'none'; + + if (name === 'templates') { + fetchTemplates().then(renderTemplateList).catch(() => { + document.querySelector('.template-list').innerHTML = + '<div id="loading">Could not reach server.</div>'; + }); + } +} + // ── Boot ────────────────────────────────────────────────────────────────────── -startPolling(); +document.addEventListener('DOMContentLoaded', () => { + updateToggleButton(); + document.getElementById('btn-toggle-completed').addEventListener('click', async () => { + setHideCompletedFailed(!getHideCompletedFailed()); + updateToggleButton(); + await poll(); + }); + + startPolling(); + + // Side panel close + document.getElementById('btn-close-panel').addEventListener('click', closeTaskPanel); + document.getElementById('task-panel-backdrop').addEventListener('click', closeTaskPanel); + + // Execution logs modal close + document.getElementById('btn-close-logs').addEventListener('click', () => { + document.getElementById('logs-modal').close(); + }); + + // Tab bar + document.querySelectorAll('.tab').forEach(btn => { + btn.addEventListener('click', () => switchTab(btn.dataset.tab)); + }); + + // Task modal + document.getElementById('btn-new-task').addEventListener('click', openTaskModal); + document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal); + + // Draft with AI button + const btnElaborate = document.getElementById('btn-elaborate'); + btnElaborate.addEventListener('click', async () => { + const prompt = document.getElementById('elaborate-prompt').value.trim(); + if (!prompt) { + const form = document.getElementById('task-form'); + // Remove previous error + const prev = form.querySelector('.form-error'); + if (prev) prev.remove(); + const errEl = document.createElement('p'); + errEl.className = 'form-error'; + errEl.textContent = 'Please enter a description before drafting.'; + form.querySelector('.elaborate-section').appendChild(errEl); + return; + } + + btnElaborate.disabled = true; + btnElaborate.textContent = 'Drafting…'; + + // Remove any previous errors or banners + const form = document.getElementById('task-form'); + form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove()); + + try { + const result = await elaborateTask(prompt); + + // Populate form fields + const f = document.getElementById('task-form'); + if (result.name) + f.querySelector('[name="name"]').value = result.name; + if (result.claude && result.claude.instructions) + f.querySelector('[name="instructions"]').value = result.claude.instructions; + if (result.claude && result.claude.working_dir) + f.querySelector('[name="working_dir"]').value = result.claude.working_dir; + if (result.claude && result.claude.model) + f.querySelector('[name="model"]').value = result.claude.model; + if (result.claude && result.claude.max_budget_usd != null) + f.querySelector('[name="max_budget_usd"]').value = result.claude.max_budget_usd; + if (result.timeout) + f.querySelector('[name="timeout"]').value = result.timeout; + if (result.priority) { + const sel = f.querySelector('[name="priority"]'); + if ([...sel.options].some(o => o.value === result.priority)) { + sel.value = result.priority; + } + } + + // Show success banner + const banner = document.createElement('p'); + banner.className = 'elaborate-banner'; + banner.textContent = 'AI draft ready — review and submit.'; + document.getElementById('task-form').querySelector('.elaborate-section').appendChild(banner); + } catch (err) { + const errEl = document.createElement('p'); + errEl.className = 'form-error'; + errEl.textContent = `Elaboration failed: ${err.message}`; + document.getElementById('task-form').querySelector('.elaborate-section').appendChild(errEl); + } finally { + btnElaborate.disabled = false; + btnElaborate.textContent = 'Draft with AI ✦'; + } + }); + + document.getElementById('task-form').addEventListener('submit', async e => { + e.preventDefault(); + + // Remove any previous error + const prev = e.target.querySelector('.form-error'); + if (prev) prev.remove(); + + const btn = e.submitter; + btn.disabled = true; + btn.textContent = 'Creating…'; + + try { + await createTask(new FormData(e.target)); + } catch (err) { + const errEl = document.createElement('p'); + errEl.className = 'form-error'; + errEl.textContent = err.message; + e.target.appendChild(errEl); + } finally { + btn.disabled = false; + btn.textContent = 'Create & Queue'; + } + }); + + // Template modal + document.getElementById('btn-new-template').addEventListener('click', openTemplateModal); + document.getElementById('btn-cancel-template').addEventListener('click', closeTemplateModal); + + document.getElementById('template-form').addEventListener('submit', async e => { + e.preventDefault(); + + // Remove any previous error + const prev = e.target.querySelector('.form-error'); + if (prev) prev.remove(); + + const btn = e.submitter; + btn.disabled = true; + btn.textContent = 'Saving…'; + + try { + await saveTemplate(new FormData(e.target)); + } catch (err) { + const errEl = document.createElement('p'); + errEl.className = 'form-error'; + errEl.textContent = err.message; + e.target.appendChild(errEl); + } finally { + btn.disabled = false; + btn.textContent = 'Save Template'; + } + }); +}); diff --git a/web/style.css b/web/style.css index d868fdf..de8ce83 100644 --- a/web/style.css +++ b/web/style.css @@ -39,6 +39,10 @@ header { position: sticky; top: 0; z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + max-width: 100%; } header h1 { @@ -47,7 +51,40 @@ header h1 { color: var(--accent); letter-spacing: -0.01em; max-width: 640px; - margin: 0 auto; + margin: 0 auto 0 0; + flex: 1; +} + +/* Tab bar */ +.tab-bar { + background: var(--surface); + border-bottom: 1px solid var(--border); + display: flex; + gap: 0; + padding: 0 1rem; + max-width: 100%; +} + +.tab { + font-size: 0.88rem; + font-weight: 600; + padding: 0.65em 1.25em; + border: none; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: color 0.15s, border-color 0.15s; + margin-bottom: -1px; +} + +.tab:hover { + color: var(--text); +} + +.tab.active { + color: var(--accent); + border-bottom-color: var(--accent); } /* Main layout */ @@ -57,6 +94,28 @@ main { padding: 1rem; } +/* Panel header */ +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; +} + +.panel-header h2 { + font-size: 1rem; + font-weight: 700; +} + +/* Task list toolbar */ +.task-list-toolbar { + display: flex; + align-items: center; + padding: 0.5rem 0; + margin-bottom: 0.75rem; + border-bottom: 1px solid var(--border); +} + /* Task list */ .task-list { display: flex; @@ -170,3 +229,478 @@ main { font-size: 0.78rem; color: var(--state-failed); } + +.hidden-tasks-info { + font-size: 0.78rem; + color: var(--text-muted); + text-align: center; + padding: 0.5rem 0; + cursor: pointer; + background: transparent; + border: none; + width: 100%; +} + +.hidden-tasks-info:hover { + color: var(--text); +} + +.hidden-tasks-fold { + display: flex; + flex-direction: column; + gap: 0.75rem; + opacity: 0.6; + margin-top: 0.5rem; +} + +/* Primary button */ +.btn-primary { + font-size: 0.85rem; + font-weight: 600; + padding: 0.4em 1em; + border-radius: 0.375rem; + border: none; + cursor: pointer; + background: #2563eb; + color: #fff; + transition: opacity 0.15s; +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Secondary button */ +.btn-secondary { + font-size: 0.85rem; + font-weight: 600; + padding: 0.4em 1em; + border-radius: 0.375rem; + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.btn-secondary:hover { + background: var(--border); + color: var(--text); +} + +.btn-secondary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Danger button */ +.btn-danger { + font-size: 0.85rem; + font-weight: 600; + padding: 0.4em 1em; + border-radius: 0.375rem; + border: none; + cursor: pointer; + background: var(--state-failed); + color: #0f172a; + transition: opacity 0.15s; +} + +.btn-danger:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-sm { + font-size: 0.78rem; + padding: 0.3em 0.75em; +} + +/* Modal dialog */ +dialog { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.5rem; + max-width: 480px; + width: 100%; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.6); +} + +dialog h2 { + font-size: 1.1rem; + font-weight: 700; + margin-bottom: 1rem; +} + +/* Form labels and inputs */ +dialog label { + display: block; + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 12px; +} + +dialog label input, +dialog label textarea, +dialog label select { + display: block; + width: 100%; + margin-top: 4px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 0.375rem; + color: var(--text); + padding: 0.4em 0.6em; + font-size: 0.9rem; + font-family: inherit; +} + +dialog label input:focus, +dialog label textarea:focus, +dialog label select:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +/* Form actions row */ +.form-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; +} + +.form-actions button[type="button"] { + font-size: 0.85rem; + padding: 0.4em 1em; + border-radius: 0.375rem; + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + cursor: pointer; +} + +.form-actions button[type="button"]:hover { + background: var(--border); + color: var(--text); +} + +/* Inline form error */ +.form-error { + color: var(--state-failed); + font-size: 0.82rem; + margin-top: 8px; +} + +/* Template list */ +.template-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +/* Template card */ +.template-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 0.875rem 1rem; + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.template-name { + font-weight: 600; + font-size: 0.95rem; +} + +.template-description { + font-size: 0.82rem; + color: var(--text-muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.template-tags { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + margin-top: 0.125rem; +} + +.tag-chip { + font-size: 0.7rem; + font-weight: 600; + padding: 0.2em 0.55em; + border-radius: 999px; + background: var(--border); + color: var(--text-muted); +} + +.template-card-footer { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.25rem; +} + +/* ── Side panel ───────────────────────────────────────────────────────────── */ + +.panel-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 20; +} + +.task-panel { + position: fixed; + top: 0; + right: 0; + width: min(480px, 100vw); + height: 100dvh; + background: var(--surface); + border-left: 1px solid var(--border); + z-index: 30; + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +.task-panel.open { + transform: translateX(0); +} + +.task-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.task-panel-header h2 { + font-size: 1rem; + font-weight: 700; + margin: 0; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.btn-close-panel { + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1.3rem; + line-height: 1; + padding: 0.15em 0.35em; + margin-left: 0.5rem; + border-radius: 0.25rem; + flex-shrink: 0; +} + +.btn-close-panel:hover { + color: var(--text); + background: var(--border); +} + +.task-panel-content { + flex: 1; + overflow-y: auto; + padding: 1.25rem; +} + +/* Panel sections */ +.panel-section { + margin-bottom: 1.5rem; +} + +.panel-section:last-child { + margin-bottom: 0; +} + +.panel-section-title { + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.625rem; + padding-bottom: 0.375rem; + border-bottom: 1px solid var(--border); +} + +.meta-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem 1rem; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.meta-item.full-width { + grid-column: 1 / -1; +} + +.meta-label { + font-size: 0.7rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.meta-value { + font-size: 0.875rem; + word-break: break-all; +} + +.meta-value.mono { + font-family: monospace; + font-size: 0.8rem; +} + +.meta-value.muted { + color: var(--text-muted); +} + +.panel-code { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 0.375rem; + padding: 0.625rem 0.75rem; + font-family: monospace; + font-size: 0.8rem; + white-space: pre-wrap; + word-break: break-word; + color: var(--text); + max-height: 220px; + overflow-y: auto; + margin-top: 0.25rem; +} + +.panel-tags { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + margin-top: 0.1rem; +} + +/* Clickable task cards */ +.task-card { + cursor: pointer; +} + +.task-card:hover { + border-color: var(--accent); +} + +/* Panel loading / error */ +.panel-loading { + color: var(--text-muted); + font-size: 0.9rem; + padding: 2rem 0; + text-align: center; +} + +.panel-fetch-error { + color: var(--state-failed); + font-size: 0.85rem; + padding: 1rem 0; +} + +/* Executions list in panel */ +.executions-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.execution-row { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 0.375rem; + padding: 0.625rem 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem 0.75rem; + flex-wrap: wrap; +} + +.execution-id { + font-family: monospace; + font-size: 0.72rem; + color: var(--text-muted); + flex-shrink: 0; +} + +.execution-times { + font-size: 0.75rem; + color: var(--text-muted); + flex: 1; + min-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.execution-cost { + font-size: 0.75rem; + color: var(--text-muted); + white-space: nowrap; +} + +.execution-exit { + font-family: monospace; + font-size: 0.72rem; + color: var(--text-muted); + white-space: nowrap; +} + +.btn-view-logs { + font-size: 0.72rem; + font-weight: 600; + padding: 0.2em 0.55em; + border-radius: 0.25rem; + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; +} + +.btn-view-logs:hover { + background: var(--border); + color: var(--text); +} + +/* Execution logs modal */ +.logs-modal-body { + display: flex; + flex-direction: column; +} + +.logs-modal-body .meta-grid { + row-gap: 0.625rem; +} |
