feat(ui): persist expand state, GPU abnormal indicator, slower auto-refresh
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<span class="gpu-thumb ${busy ? 'busy' : 'idle'}" title="#${gpu.index} GPU ${gpu.gpuUtilizationPercent}% · MEM ${memoryPercent}%"></span>`;
|
||||
const thumbClass = isGpuAbnormal(gpu) ? 'abnormal' : isGpuIdle(gpu) ? 'idle' : 'busy';
|
||||
return `<span class="gpu-thumb ${thumbClass}" title="#${gpu.index} GPU ${gpu.gpuUtilizationPercent}% · MEM ${memoryPercent}%"></span>`;
|
||||
}).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 = `<pre class="terminal-body" tabindex="0">${escapeHtml(content)}</pre>`;
|
||||
if (!expanded) {
|
||||
return `<details class="terminal ${tone}">
|
||||
const openAttr = terminalKey && state.expandedTerminals.has(terminalKey) ? ' open' : '';
|
||||
return `<details class="terminal ${tone}" data-terminal-key="${escapeHtml(terminalKey)}"${openAttr}>
|
||||
<summary class="terminal-head">
|
||||
<span>${label}</span>
|
||||
<span>${lineCount} lines</span>
|
||||
@@ -343,8 +351,8 @@ function taskFullBody(task, { expandedTerminals }) {
|
||||
</div>
|
||||
<div class="task-meta workspace-path">${escapeHtml(task.workspace)}</div>
|
||||
<div class="task-prompt">${escapeHtml(task.prompt)}</div>
|
||||
${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)
|
||||
? `<button class="secondary-button small" type="button" data-resume-task="${escapeHtml(task.id)}">选择继续</button>`
|
||||
: '';
|
||||
return `<details class="task-card history-task status-${escapeHtml(task.status)}">
|
||||
const openAttr = state.expandedTasks.has(task.id) ? ' open' : '';
|
||||
return `<details class="task-card history-task status-${escapeHtml(task.status)}" data-task-id="${escapeHtml(task.id)}"${openAttr}>
|
||||
<summary class="task-summary">
|
||||
<span class="chip status-chip">${escapeHtml(statusLabel(task.status))}</span>
|
||||
<span class="task-summary-main">
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user