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);