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,
|
selectedRepoId: null,
|
||||||
selectedBranch: null,
|
selectedBranch: null,
|
||||||
expandedGpuHosts: new Set(),
|
expandedGpuHosts: new Set(),
|
||||||
|
expandedTasks: new Set(),
|
||||||
|
expandedTerminals: new Set(),
|
||||||
settings: { gpuHosts: [] },
|
settings: { gpuHosts: [] },
|
||||||
errors: {}
|
errors: {}
|
||||||
};
|
};
|
||||||
@@ -72,7 +74,12 @@ function shortSession(sessionId) {
|
|||||||
|
|
||||||
function isGpuIdle(gpu) {
|
function isGpuIdle(gpu) {
|
||||||
const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB);
|
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 = '') {
|
function toast(message, tone = '') {
|
||||||
@@ -169,8 +176,8 @@ function renderGpus() {
|
|||||||
const expanded = state.expandedGpuHosts.has(host.host);
|
const expanded = state.expandedGpuHosts.has(host.host);
|
||||||
const thumbRows = host.gpus.map((gpu) => {
|
const thumbRows = host.gpus.map((gpu) => {
|
||||||
const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB);
|
const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB);
|
||||||
const busy = !isGpuIdle(gpu);
|
const thumbClass = isGpuAbnormal(gpu) ? 'abnormal' : isGpuIdle(gpu) ? 'idle' : 'busy';
|
||||||
return `<span class="gpu-thumb ${busy ? 'busy' : 'idle'}" title="#${gpu.index} GPU ${gpu.gpuUtilizationPercent}% · MEM ${memoryPercent}%"></span>`;
|
return `<span class="gpu-thumb ${thumbClass}" title="#${gpu.index} GPU ${gpu.gpuUtilizationPercent}% · MEM ${memoryPercent}%"></span>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
const gpuRows = host.gpus.map((gpu) => {
|
const gpuRows = host.gpus.map((gpu) => {
|
||||||
const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB);
|
const memoryPercent = percent(gpu.memoryUsedMiB, gpu.memoryTotalMiB);
|
||||||
@@ -299,12 +306,13 @@ function renderResumeTasks() {
|
|||||||
$('resumeTaskButton').disabled = options.length === 0;
|
$('resumeTaskButton').disabled = options.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTerminal(label, content, tone = '', expanded = true) {
|
function renderTerminal(label, content, tone = '', expanded = true, terminalKey = '') {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
const lineCount = content.split('\n').length;
|
const lineCount = content.split('\n').length;
|
||||||
const body = `<pre class="terminal-body" tabindex="0">${escapeHtml(content)}</pre>`;
|
const body = `<pre class="terminal-body" tabindex="0">${escapeHtml(content)}</pre>`;
|
||||||
if (!expanded) {
|
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">
|
<summary class="terminal-head">
|
||||||
<span>${label}</span>
|
<span>${label}</span>
|
||||||
<span>${lineCount} lines</span>
|
<span>${lineCount} lines</span>
|
||||||
@@ -343,8 +351,8 @@ function taskFullBody(task, { expandedTerminals }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="task-meta workspace-path">${escapeHtml(task.workspace)}</div>
|
<div class="task-meta workspace-path">${escapeHtml(task.workspace)}</div>
|
||||||
<div class="task-prompt">${escapeHtml(task.prompt)}</div>
|
<div class="task-prompt">${escapeHtml(task.prompt)}</div>
|
||||||
${renderTerminal('stderr', task.stderr, 'stderr', expandedTerminals)}
|
${renderTerminal('stderr', task.stderr, 'stderr', expandedTerminals, `${task.id}:stderr`)}
|
||||||
${renderTerminal('stdout', task.stdout, '', expandedTerminals)}
|
${renderTerminal('stdout', task.stdout, '', expandedTerminals, `${task.id}:stdout`)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +368,8 @@ function renderHistoryTaskCard(task) {
|
|||||||
const resumeButton = canResumeTask(task)
|
const resumeButton = canResumeTask(task)
|
||||||
? `<button class="secondary-button small" type="button" data-resume-task="${escapeHtml(task.id)}">选择继续</button>`
|
? `<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">
|
<summary class="task-summary">
|
||||||
<span class="chip status-chip">${escapeHtml(statusLabel(task.status))}</span>
|
<span class="chip status-chip">${escapeHtml(statusLabel(task.status))}</span>
|
||||||
<span class="task-summary-main">
|
<span class="task-summary-main">
|
||||||
@@ -577,6 +586,24 @@ $('taskBoard').addEventListener('click', (event) => {
|
|||||||
selectResumeTask(button.dataset.resumeTask);
|
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) {
|
async function submitTask(resume) {
|
||||||
const repo = state.repos.find((item) => item.id === state.selectedRepoId);
|
const repo = state.repos.find((item) => item.id === state.selectedRepoId);
|
||||||
const prompt = $('promptInput').value.trim();
|
const prompt = $('promptInput').value.trim();
|
||||||
@@ -625,4 +652,4 @@ $('taskForm').addEventListener('submit', async (event) => {
|
|||||||
$('resumeTaskButton').addEventListener('click', () => submitTask(true));
|
$('resumeTaskButton').addEventListener('click', () => submitTask(true));
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 30000);
|
setInterval(refresh, 60000);
|
||||||
|
|||||||
@@ -345,6 +345,11 @@ h3 {
|
|||||||
border-color: color-mix(in srgb, var(--good) 45%, transparent);
|
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 {
|
.gpu-thumb.busy {
|
||||||
background: color-mix(in srgb, var(--bad) 22%, transparent);
|
background: color-mix(in srgb, var(--bad) 22%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--bad) 50%, transparent);
|
border-color: color-mix(in srgb, var(--bad) 50%, transparent);
|
||||||
|
|||||||
Reference in New Issue
Block a user