mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
Merge pull request #692: feat(header): opt-in GPU and CPU clock readings after CPU and MEM
Conflict in config/test-suites.ts resolved by keeping both sides: #689's header-divider and #692's header-host-sensors browser tests are both listed in BROWSER_TEST_GLOBS. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
c7b6226cb0
19 files changed
+1389
-17
No files matched your search
+5
-10
@@ -2435,8 +2435,6 @@ class CodemanApp {
|
||||
if (data.parentSessionId) this.markConnectionLineEntering?.('lineage:' + data.id);
|
||||
this.renderSessionTabs();
|
||||
this.updateCost();
|
||||
// Start stats polling when first session appears
|
||||
if (this.sessions.size === 1) this.startSystemStatsPolling();
|
||||
if (this._urlSessionId === data.id) this._selectUrlSession();
|
||||
}
|
||||
|
||||
@@ -2492,8 +2490,8 @@ class CodemanApp {
|
||||
this.renderSessionTabs();
|
||||
this.renderRalphStatePanel(); // Update ralph panel after session deleted
|
||||
this.renderProjectInsightsPanel(); // Update project insights panel after session deleted
|
||||
// Stop stats polling when no sessions remain
|
||||
if (this.sessions.size === 0) this.stopSystemStatsPolling();
|
||||
// Stats polling keeps going with no session left: the header shows CPU, MEM and the opt-in readings
|
||||
// regardless, and would sit at "--" otherwise.
|
||||
}
|
||||
|
||||
// SSE wrappers — skip terminal events while WebSocket owns active terminal I/O.
|
||||
@@ -5064,12 +5062,9 @@ class CodemanApp {
|
||||
// survive reloads and SSE reconnects (methods in approvals-ui.js).
|
||||
this.seedApprovals?.();
|
||||
|
||||
// Start/stop system stats polling based on session count
|
||||
if (this.sessions.size > 0) {
|
||||
this.startSystemStatsPolling();
|
||||
} else {
|
||||
this.stopSystemStatsPolling();
|
||||
}
|
||||
// Poll the header stats whether or not a session is open (fetchSystemStats skips the call while the
|
||||
// stats pill is hidden); starting restarts the interval, so only start it once.
|
||||
if (!this.systemStatsInterval) this.startSystemStatsPolling();
|
||||
|
||||
// CRITICAL: Clean up all floating windows before loading new subagents
|
||||
// This prevents memory leaks from ResizeObservers, EventSources, and DOM elements
|
||||
|
||||
@@ -451,6 +451,8 @@
|
||||
'Header Displays': '顶部栏显示',
|
||||
'Font Controls': '字体控制',
|
||||
'System Stats': '系统状态',
|
||||
'GPU Stats': 'GPU 状态',
|
||||
'CPU MHz': 'CPU 频率',
|
||||
'Lifecycle Log': '生命周期日志',
|
||||
'Response Viewer': '回复查看器',
|
||||
'Attachments Button': '附件按钮',
|
||||
|
||||
@@ -178,6 +178,30 @@
|
||||
</div>
|
||||
<span class="stat-value" id="statMem">--</span>
|
||||
</div>
|
||||
<div class="stat-item" id="statGpuItem" hidden>
|
||||
<span class="stat-ring" id="statGpuRing" aria-hidden="true"></span>
|
||||
<span class="stat-label">GPU</span>
|
||||
<div class="stat-bar">
|
||||
<div class="stat-bar-fill stat-bar-cpu" id="statGpuBar"></div>
|
||||
</div>
|
||||
<span class="stat-value" id="statGpu">--%</span>
|
||||
</div>
|
||||
<div class="stat-item" id="statVramItem" hidden>
|
||||
<span class="stat-ring" id="statVramRing" aria-hidden="true"></span>
|
||||
<span class="stat-label">VRAM</span>
|
||||
<div class="stat-bar">
|
||||
<div class="stat-bar-fill stat-bar-cpu" id="statVramBar"></div>
|
||||
</div>
|
||||
<span class="stat-value" id="statVram">--%</span>
|
||||
</div>
|
||||
<div class="stat-item" id="statGpuMhzItem" hidden>
|
||||
<span class="stat-label">GPU</span>
|
||||
<span class="stat-value" id="statGpuMhz">-- MHz</span>
|
||||
</div>
|
||||
<div class="stat-item" id="statCpuMhzItem" hidden>
|
||||
<span class="stat-label">CPU</span>
|
||||
<span class="stat-value" id="statCpuMhz">-- MHz</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-icon-header btn-redraw-terminal btn-redraw-terminal--hidden" onclick="app.restoreTerminalSize()" title="Redraw terminal to fit current screen (Ctrl+Shift+R)" aria-label="Redraw terminal"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg></button>
|
||||
<button class="btn-icon-header btn-response-viewer-header btn-response-viewer-header--hidden" onclick="app.toggleResponseViewer()" title="View last response" aria-label="View last response"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
||||
@@ -2097,6 +2121,8 @@
|
||||
<div class="set-chips" data-search="header buttons plan usage font stats lifecycle response file viewer attachments monitor session away cron redraw split tiles">
|
||||
<label class="set-chip" data-preview="header" data-preview-order="1" data-preview-text="A+"><input type="checkbox" id="appSettingsShowFontControls"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20 10 5l6 15"/><path d="M6.5 15h7"/><path d="M18 12h4M20 10v4"/></svg><span>Font Size</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="2" data-preview-text="CPU 12%"><input type="checkbox" id="appSettingsShowSystemStats" checked><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l2.5-7 4 14L16 12h5"/></svg><span>System Stats</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="2" data-preview-text="GPU 8%"><input type="checkbox" id="appSettingsShowGpuStats"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="7" width="18" height="10" rx="2"/><path d="M7 21v-4M12 21v-4M17 21v-4M7 7V3M17 7V3"/></svg><span>GPU Stats</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="2" data-preview-text="3400 MHz"><input type="checkbox" id="appSettingsShowCpuMhz"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span>CPU MHz</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="3"><input type="checkbox" id="appSettingsShowRedrawButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg><span>Redraw Terminal</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="4"><input type="checkbox" id="appSettingsShowResponseViewer"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg><span>Response Viewer</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="5"><input type="checkbox" id="appSettingsShowAwayDigestButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg><span>Away Digest</span></label>
|
||||
|
||||
@@ -6859,13 +6859,29 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Skip polling when system stats display is hidden
|
||||
const statsEl = document.getElementById('headerSystemStats');
|
||||
if (!statsEl || statsEl.style.display === 'none') return;
|
||||
// One request at a time: the poll fires every 2 s, and a slow answer (a cold WSL sampler, a hung
|
||||
// server) must not pile up requests behind it.
|
||||
if (this._systemStatsPending) return;
|
||||
this._systemStatsPending = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/system/stats');
|
||||
// Opt-in readings are only requested while their header setting is on (default off).
|
||||
const settings = this.loadAppSettingsFromStorage?.() ?? {};
|
||||
const wanted = ['gpu', settings.showGpuStats, 'cpumhz', settings.showCpuMhz];
|
||||
const query = [];
|
||||
for (let i = 0; i < wanted.length; i += 2) if (wanted[i + 1] === true) query.push(`${wanted[i]}=1`);
|
||||
const res = await fetch('/api/system/stats' + (query.length ? '?' + query.join('&') : ''), {
|
||||
signal:
|
||||
typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
||||
? AbortSignal.timeout(8000)
|
||||
: undefined,
|
||||
});
|
||||
const stats = await res.json();
|
||||
this.updateSystemStatsDisplay(stats.data);
|
||||
} catch (err) {
|
||||
// Silently fail - system stats are not critical
|
||||
} finally {
|
||||
this._systemStatsPending = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6910,6 +6926,58 @@ Object.assign(CodemanApp.prototype, {
|
||||
memBar.classList.add('medium');
|
||||
}
|
||||
}
|
||||
|
||||
// After the MEM block, so nothing in the optional readings can stop MEM updating.
|
||||
this._updateHostSensors(stats);
|
||||
},
|
||||
|
||||
/**
|
||||
* The opt-in readings after CPU and MEM: GPU load, GPU memory and GPU clock (the GPU Stats setting) and the CPU clock. An item
|
||||
* shows only while its setting is on AND the host reported it, so a machine without a readable GPU just
|
||||
* has nothing extra in the header.
|
||||
*/
|
||||
_updateHostSensors(stats) {
|
||||
const settings = this.loadAppSettingsFromStorage?.() ?? {};
|
||||
const show = (id, visible) => {
|
||||
const el = this.$(id);
|
||||
if (el) el.hidden = !visible;
|
||||
};
|
||||
const fill = (valueId, ringId, barId, percent, text) => {
|
||||
const value = this.$(valueId);
|
||||
if (value) {
|
||||
value.textContent = text;
|
||||
value.classList.toggle('high', percent > 80);
|
||||
}
|
||||
this._setStatRing(ringId, percent);
|
||||
const bar = this.$(barId);
|
||||
if (bar) bar.style.width = `${Math.min(100, percent)}%`;
|
||||
};
|
||||
const gpu = settings.showGpuStats === true ? stats.gpu : undefined;
|
||||
show('statGpuItem', Boolean(gpu));
|
||||
if (gpu) fill('statGpu', 'statGpuRing', 'statGpuBar', gpu.percent, `${gpu.percent}%`);
|
||||
// Graphics memory: the amount in use whenever it is known, with a ring only when the total is too (an
|
||||
// integrated GPU under WSL, where Windows reports usage but the share of RAM is a guess, may have none).
|
||||
show('statVramItem', Boolean(gpu && gpu.memUsedMB !== undefined));
|
||||
if (gpu && gpu.memUsedMB !== undefined) {
|
||||
const used = `${(gpu.memUsedMB / 1024).toFixed(1)}G`;
|
||||
const ring = this.$('statVramRing');
|
||||
if (gpu.memPercent !== undefined) {
|
||||
fill('statVram', 'statVramRing', 'statVramBar', gpu.memPercent, used);
|
||||
if (ring) ring.style.display = '';
|
||||
} else {
|
||||
const vram = this.$('statVram');
|
||||
if (vram) vram.textContent = used;
|
||||
if (ring) ring.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// The GPU clock is a third GPU Stats item (the host may not report one: Windows has none for Intel).
|
||||
show('statGpuMhzItem', Boolean(gpu && gpu.mhz !== undefined));
|
||||
const gpuMhzEl = gpu && gpu.mhz !== undefined ? this.$('statGpuMhz') : null;
|
||||
if (gpuMhzEl) gpuMhzEl.textContent = `${gpu.mhz} MHz`;
|
||||
const cpuMhz = settings.showCpuMhz === true && stats.cpuMhz !== undefined ? stats.cpuMhz : undefined;
|
||||
show('statCpuMhzItem', cpuMhz !== undefined);
|
||||
const cpuMhzEl = cpuMhz !== undefined ? this.$('statCpuMhz') : null;
|
||||
if (cpuMhzEl) cpuMhzEl.textContent = `${cpuMhz} MHz`;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -514,6 +514,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Header visibility settings
|
||||
document.getElementById('appSettingsShowFontControls').checked = settings.showFontControls ?? defaults.showFontControls ?? false;
|
||||
document.getElementById('appSettingsShowSystemStats').checked = settings.showSystemStats ?? defaults.showSystemStats ?? true;
|
||||
// Opt-in host readings after CPU and MEM: all default OFF.
|
||||
document.getElementById('appSettingsShowGpuStats').checked = settings.showGpuStats ?? defaults.showGpuStats ?? false;
|
||||
document.getElementById('appSettingsShowCpuMhz').checked = settings.showCpuMhz ?? defaults.showCpuMhz ?? false;
|
||||
document.getElementById('appSettingsHeaderStatsStyle').value = this.resolveHeaderStatsStyle(settings);
|
||||
document.getElementById('appSettingsShowLifecycleLog').checked = settings.showLifecycleLog ?? defaults.showLifecycleLog ?? false;
|
||||
document.getElementById('appSettingsShowResponseViewer').checked = settings.showResponseViewer ?? defaults.showResponseViewer ?? false;
|
||||
@@ -2904,6 +2907,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Header visibility settings
|
||||
showFontControls: document.getElementById('appSettingsShowFontControls').checked,
|
||||
showSystemStats: document.getElementById('appSettingsShowSystemStats').checked,
|
||||
showGpuStats: document.getElementById('appSettingsShowGpuStats').checked,
|
||||
showCpuMhz: document.getElementById('appSettingsShowCpuMhz').checked,
|
||||
headerStatsStyle: document.getElementById('appSettingsHeaderStatsStyle').value,
|
||||
showLifecycleLog: document.getElementById('appSettingsShowLifecycleLog').checked,
|
||||
showResponseViewer: document.getElementById('appSettingsShowResponseViewer').checked,
|
||||
@@ -3234,6 +3239,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
showCronButton: _crb,
|
||||
// Per-device bottom-bar indicator, absent from SettingsUpdateSchema (.strict()): it must not reach the PUT.
|
||||
showGitStatus: _sgs,
|
||||
// Opt-in header readings (GPU Stats, CPU MHz): per-device and absent from the .strict() schema.
|
||||
showGpuStats: _sgp,
|
||||
showCpuMhz: _scm,
|
||||
gitStatusTree: _gst,
|
||||
gitStatusMaxRepos: _gsm,
|
||||
gitStatusTimeoutSeconds: _gst2,
|
||||
@@ -4113,6 +4121,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Header visibility - hide everything on mobile
|
||||
showFontControls: false,
|
||||
showSystemStats: false,
|
||||
showGpuStats: false,
|
||||
showCpuMhz: false,
|
||||
showTokenCount: false,
|
||||
showCost: false,
|
||||
// Panel visibility - hide panels on mobile (not enough space)
|
||||
@@ -4880,7 +4890,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
*/
|
||||
_displaySettingKeySet() {
|
||||
const displayKeys = new Set([
|
||||
'showFontControls', 'showSystemStats', 'headerStatsStyle', 'showTokenCount', 'showCost',
|
||||
'showFontControls', 'showSystemStats', 'showGpuStats', 'showCpuMhz', 'headerStatsStyle', 'showTokenCount', 'showCost',
|
||||
'showLifecycleLog', 'showResponseViewer', 'showRedrawButton',
|
||||
'showMonitor', 'showProjectInsights', 'showFileBrowser', 'showSubagents',
|
||||
'subagentActiveTabOnly', 'tabTwoRows', 'showTabCliLogos', 'tabOrientation', 'tabRailWidth', 'tabRailDetail', 'tabRailSort', 'tabArrangement', 'tabStateOrder', 'sessionListLayout', 'sessionSidebarFontSize', 'localEchoEnabled', 'cjkInputEnabled', 'extendedKeyboardBar',
|
||||
|
||||
@@ -2356,6 +2356,13 @@ body.solo-mode .header-divider {
|
||||
}
|
||||
}
|
||||
|
||||
/* The opt-in host readings (GPU, VRAM, MHz) start hidden and are shown by panels-ui.js once the host
|
||||
reports them; a plain `display` on .stat-item would override the attribute, and the Tiles style sets its own
|
||||
(a grid) at a higher specificity, so the selector carries the style attribute to outrank it. */
|
||||
html[data-header-stats] .header-system-stats .stat-item[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Connection Indicator */
|
||||
.connection-indicator {
|
||||
display: flex;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { totalmem, freemem, loadavg, cpus } from 'node:os';
|
||||
import { execSync, spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { dataPath } from '../../config/instance.js';
|
||||
import { readHostSensors } from '../../host-sensors.js';
|
||||
import { ApiErrorCode, createErrorResponse, getErrorMessage, type NiceConfig } from '../../types.js';
|
||||
import { isUnauthenticatedNetworkAcknowledged } from '../network-auth-policy.js';
|
||||
import { isMultiUserMode } from '../../config/multiuser.js';
|
||||
@@ -1007,8 +1008,15 @@ export function registerSystemRoutes(
|
||||
|
||||
// ========== System Stats ==========
|
||||
|
||||
app.get('/api/system/stats', async () => {
|
||||
return getSystemStats();
|
||||
// GPU (load, memory, clock) and CPU clock readings are opt-in: the browser adds ?gpu=1 and ?cpumhz=1 only while
|
||||
// the matching header setting is on, so a default install never runs nvidia-smi or reads sysfs.
|
||||
app.get('/api/system/stats', async (req) => {
|
||||
const query = req.query as { gpu?: string; cpumhz?: string };
|
||||
const sensors = await readHostSensors({
|
||||
gpu: query.gpu === '1',
|
||||
cpuMhz: query.cpumhz === '1',
|
||||
});
|
||||
return { ...getSystemStats(), ...sensors };
|
||||
});
|
||||
|
||||
// ========== Settings ==========
|
||||
|
||||
@@ -45,6 +45,7 @@ import { looksLikeHostReboot, newestPersistedActivity, planRebootRestore } from
|
||||
import { rebootRestoreRegistry } from './reboot-restore-registry.js';
|
||||
import { dataPath, getDataDir, CODEMAN_INSTANCE } from '../config/instance.js';
|
||||
import { WebhookNotifier, readWebhookConfig } from '../webhook-notify.js';
|
||||
import { stopHostSensors } from '../host-sensors.js';
|
||||
import type { WebhookUrgency } from '../types/push.js';
|
||||
import { webviewFetch } from './webview-egress.js';
|
||||
import { readRemoteHosts, rehydrateRemoteHostFields } from '../remote-hosts.js';
|
||||
@@ -4175,6 +4176,9 @@ export class WebServer extends EventEmitter {
|
||||
// got wrong once.
|
||||
void stopDeepSeekWeb();
|
||||
|
||||
// The header's GPU / CPU clock sampler is a Windows child process under WSL, and its idle sweep a timer.
|
||||
stopHostSensors();
|
||||
|
||||
// Same teardown rule: the per-endpoint llama-swap log tails are otherwise closed
|
||||
// only by the periodic idle sweep, whose interval is disposed just below.
|
||||
closeAllLlamaSwapLogTails();
|
||||
|
||||
Reference in new issue
Block a user