From bf926e327f4a7105a45a22c83b3d2a678778e6a8 Mon Sep 17 00:00:00 2001 From: Gahow Wang Date: Fri, 15 May 2026 15:08:40 +0800 Subject: [PATCH] feat(ui): persist expand state, GPU abnormal indicator, slower auto-refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remember history task and stdout/stderr terminal open state across the 60s refresh so they no longer collapse on every poll. - Mark GPUs with allocated memory but near-zero utilization as `abnormal` (orange) — covers stuck processes that previously rendered as busy. - Bump dashboard auto-refresh from 30s to 60s. Co-Authored-By: Claude Opus 4.7 --- public/app.js | 45 ++++++++++++++++++++++++++++++++++++--------- public/styles.css | 5 +++++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/public/app.js b/public/app.js index 7de4bf0..a7a6232 100644 --- a/public/app.js +++ b/public/app.js @@ -8,6 +8,8 @@ let state = { selectedRepoId: null, selectedBranch: null, expandedGpuHosts: new Set(), + expandedTasks: new Set(), + expandedTerminals: new Set(), settings: { gpuHosts: [] }, errors: {} }; @@ -72,7 +74,12 @@ function shortSession(sessionId) { function isGpuIdle(gpu) { const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB); - return memoryPercent < 5 && gpu.gpuUtilizationPercent < 5; + return memoryPercent < 5 || gpu.gpuUtilizationPercent < 5; +} + +function isGpuAbnormal(gpu) { + const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB); + return memoryPercent >= 5 && gpu.gpuUtilizationPercent < 5; } function toast(message, tone = '') { @@ -169,8 +176,8 @@ function renderGpus() { const expanded = state.expandedGpuHosts.has(host.host); const thumbRows = host.gpus.map((gpu) => { const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB); - const busy = !isGpuIdle(gpu); - return ``; + const thumbClass = isGpuAbnormal(gpu) ? 'abnormal' : isGpuIdle(gpu) ? 'idle' : 'busy'; + return ``; }).join(''); const gpuRows = host.gpus.map((gpu) => { const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB); @@ -299,12 +306,13 @@ function renderResumeTasks() { $('resumeTaskButton').disabled = options.length === 0; } -function renderTerminal(label, content, tone = '', expanded = true) { +function renderTerminal(label, content, tone = '', expanded = true, terminalKey = '') { if (!content) return ''; const lineCount = content.split('\n').length; const body = `
${escapeHtml(content)}
`; if (!expanded) { - return `
+ const openAttr = terminalKey && state.expandedTerminals.has(terminalKey) ? ' open' : ''; + return `
${label} ${lineCount} lines @@ -343,8 +351,8 @@ function taskFullBody(task, { expandedTerminals }) {
${escapeHtml(task.workspace)}
${escapeHtml(task.prompt)}
- ${renderTerminal('stderr', task.stderr, 'stderr', expandedTerminals)} - ${renderTerminal('stdout', task.stdout, '', expandedTerminals)} + ${renderTerminal('stderr', task.stderr, 'stderr', expandedTerminals, `${task.id}:stderr`)} + ${renderTerminal('stdout', task.stdout, '', expandedTerminals, `${task.id}:stdout`)} `; } @@ -360,7 +368,8 @@ function renderHistoryTaskCard(task) { const resumeButton = canResumeTask(task) ? `` : ''; - return `
+ const openAttr = state.expandedTasks.has(task.id) ? ' open' : ''; + return `
${escapeHtml(statusLabel(task.status))} @@ -577,6 +586,24 @@ $('taskBoard').addEventListener('click', (event) => { selectResumeTask(button.dataset.resumeTask); }); +$('taskBoard').addEventListener('toggle', (event) => { + const target = event.target; + if (!(target instanceof HTMLDetailsElement)) return; + if (target.matches('details.history-task')) { + const id = target.dataset.taskId; + if (!id) return; + if (target.open) state.expandedTasks.add(id); + else state.expandedTasks.delete(id); + return; + } + if (target.matches('details.terminal')) { + const key = target.dataset.terminalKey; + if (!key) return; + if (target.open) state.expandedTerminals.add(key); + else state.expandedTerminals.delete(key); + } +}, true); + async function submitTask(resume) { const repo = state.repos.find((item) => item.id === state.selectedRepoId); const prompt = $('promptInput').value.trim(); @@ -625,4 +652,4 @@ $('taskForm').addEventListener('submit', async (event) => { $('resumeTaskButton').addEventListener('click', () => submitTask(true)); refresh(); -setInterval(refresh, 30000); +setInterval(refresh, 60000); diff --git a/public/styles.css b/public/styles.css index 1b96f5f..5341ef3 100644 --- a/public/styles.css +++ b/public/styles.css @@ -345,6 +345,11 @@ h3 { border-color: color-mix(in srgb, var(--good) 45%, transparent); } +.gpu-thumb.abnormal { + background: color-mix(in srgb, var(--warn) 22%, transparent); + border-color: color-mix(in srgb, var(--warn) 50%, transparent); +} + .gpu-thumb.busy { background: color-mix(in srgb, var(--bad) 22%, transparent); border-color: color-mix(in srgb, var(--bad) 50%, transparent);