GNX001/dsh-system-monitor

dsh-system-monitor

DeepSeek Harness 的悬浮状态条:一行纯文字显示 CPU、内存、多 GPU 的温度与占用率,以及实时网速。零运行时依赖,数据仅在本机回环地址读取。

Uncategorizedcpudeepseek-harnessgpu

dsh-system-monitor

Author: DeepSeek + DeepSeek-Harness

中文 | English

A floating status capsule for the DeepSeek Harness web GUI: CPU, memory, every GPU and network throughput as one line of plain text.

  ⣿ CPU 9.6% 78.9°C 丨 MEM 43% 13.4/31.2 GB 丨 GPU 0% 51°C 0/11.9 GB 丨 网速 ↓ 1.3 MB/s ↑ 240 KB/s  ⟳ ✕

(A layout sketch, not a screenshot. The real colors come from your DSH theme — see Theming and stacking.)

Features

  • One line, text only — a horizontal capsule: drag grip, then each metric separated by 丨, then refresh and hide. No bars, gauges or sparklines; it is a readout, not a chart. It wraps onto a second line rather than clipping a metric on a narrow window.
  • CPU — utilization and temperature. The full model name and thread count are in the item's tooltip, so the line stays short without losing them.
  • Memory — used/total in a shared unit, using the same "available" accounting Windows Task Manager and macOS Activity Monitor show.
  • Every GPU — one item per adapter with utilization, temperature, VRAM and (optionally) power draw. A hybrid laptop reports both the discrete and the integrated GPU.
  • 网速 / NET — download and upload throughput, from the operating system's own network counters, with loopback and virtual adapters excluded so the total is not double-counted.
  • Follows your DSH theme — every color is a DSH design token, so the capsule matches the built-in light/dark switch and third-party themes such as Catppuccin, and re-tints the instant you change theme.
  • Floating and draggable — drag it by anywhere on the capsule (buttons excluded); the position and opacity persist across reloads.
  • Honest about missing data — a counter the platform does not implement shows —, never a fabricated 0. A missing vendor tool degrades one metric, it never blanks the capsule.
  • Loopback-only, read-only — the host half serves two GET routes fenced to the local machine. No writes, no network egress, no elevation, no drivers.
  • Bilingual — English and Chinese, following the DSH locale setting.

Requirements

DeepSeek Harness>= 0.1.5-rc.1, web or desktop profile
Node>= 20 (the version Harness itself ships is fine)
Runtime dependenciesnone

Platform coverage for each reading:

ReadingWindowsLinuxmacOS
CPU utilization✅ os.cpus()✅✅
CPU temperature✅ ACPI thermal zones via typeperf✅ /sys/class/thermal + coretemp/k10temp❌ needs root (powermetrics)
Memory✅✅✅
GPU utilization / temp / VRAM / power✅ NVIDIA via nvidia-smi; utilization-only fallback via Windows GPU performance counters✅ NVIDIA via nvidia-smi; AMD via amdgpu sysfs❌
Network throughput✅ PDH network counters via typeperf✅ /proc/net/dev❌

Row by row: CPU usage, memory and network work everywhere the operating system publishes counters. GPU and CPU temperature need a source the operating system actually provides; where it does not exist, that one reading is blank and the rest keep working.

Install

dsh plugin --profile web add github:GNX001/dsh-system-monitor

No build step is needed: the bundled browser half (lib/client.js) is committed, so installing from git is enough. There is no npm release yet.

From a local checkout

git clone https://github.com/GNX001/dsh-system-monitor.git
cd dsh-system-monitor
dsh plugin --profile web add link:$(pwd)

From npm (once published)

dsh plugin --profile web add dsh-system-monitor

Restart dsh web (or DSH Desktop) after installing. The tile appears in the top-right corner, and Settings → System monitor tile gains a section for it.

DSH Desktop note. The desktop shell starts the Harness server itself, so a plugin install only takes effect on the next restart of DSH Desktop. If dsh is not on your PATH, the same command works through the copy the desktop ships — for example node "$env:APPDATA\dsh-desktop\harness\profiles\node_modules\@deepseek-ai\dsh\lib\bin.js" plugin --profile web add github:GNX001/dsh-system-monitor.

Usage

ActionHow
Move the capsuleDrag anywhere on it except its buttons
Refresh immediatelyThe ⟳ button (forces a fresh hardware probe)
Hide the capsuleThe ✕ button — reopen it in Settings → System monitor tile
ConfigureSettings → System monitor tile

Everything configurable lives in DSH's own Settings, not in a popover on the capsule:

SettingDefaultNotes
Show the floating capsuleonThe way back after hiding it
Refresh interval1.5 s1 s – 10 s
Opacity94 %40 % – 100 %
CPU / CPU temperature / Memory / GPUonItem visibility
GPU temperature / VRAM / power drawtemp + VRAM on, power off
Network speed (up and down)onBoth directions always shown together
Compact modeoffTighter padding and type
Reset position / Restore defaults—

Theming and stacking

Colors. The capsule owns no palette. Every color is a DSH design token (--dsw-alias-bg-layer-2, --dsw-alias-label-primary, --dsw-alias-state-warn-primary, …), which the theme plugin declares on body for the light theme and on body[data-ds-dark-theme] for the dark one. The capsule is a child of body, so it inherits those declarations and the browser re-resolves them the moment the theme changes — no listener, no re-render, and third-party themes such as Catppuccin and neu-theme are followed because they rewrite the same tokens. Each reference carries a literal fallback, so it still renders where the tokens are absent.

Stacking. The capsule is fixed at z-index: 900. That has to sit between two layers: conversation content (code blocks and tool cards use z-index: 1–12, and because none of their ancestors creates a stacking context they paint over a z-index: auto fixed element) and DSH's own menus and modals (1000+). So the capsule covers a code block, and opening a menu still covers the capsule.

Configuration

The tile's own options live in the browser. The host half is configured in the profile's cordis.patch.yml, under the row this plugin inserts:

- insert:
    - id: system-monitor
      name: dsh-system-monitor
      config:
        # All optional; these are the defaults.
        tickMs: 1000                  # CPU/memory cadence (in-process, cheap)
        gpuMs: 1500                   # GPU probe cadence (spawns nvidia-smi)
        cpuTemperatureMs: 5000        # CPU temperature cadence (spawns typeperf)
        networkMs: 3000               # network throughput cadence (spawns typeperf)
        cpuTemperatureScale: auto     # auto | kelvin | decikelvin | decicelsius
        gpu: true                     # false = never probe GPUs
        cpuTemperature: true          # false = never probe CPU temperature
        network: true                 # false = never probe network throughput
        networkExclude: []            # adapter name fragments to ignore (see below)
        networkInclude: []            # fragments to count even if excluded
        allowRefresh: true            # false = reject ?refresh=1
        nvidiaSmiPath: nvidia-smi     # name or absolute path
        enabled: true                 # false = mount nothing at all

networkExclude defaults to a list of loopback, VPN/overlay and virtual adapter fragments (loopback, pseudo, virtual, isatap, teredo, vmware, wsl, wireguard, tailscale, docker, and so on), plus any Windows duplicate instance suffixed _2. That matters: a loopback or VPN adapter carries the same packets as the physical one, so counting both doubles the reading — and a local dev server alone can produce gigabytes of loopback traffic. The snapshot lists every adapter with a counted flag, so you can see exactly what was included before overriding anything.

cpuTemperatureScale exists because ACPI thermal zones are reported in different units by different providers; auto detects the unit from the magnitude, and the override is there for sources that disagree. See How each number is obtained.

HTTP API

Both routes are GET, loopback-only, and Cache-Control: no-store.

RoutePurpose
/api/dsh-system-monitor/snapshotThe current reading (served from a cached sample; no IO in the request path)
/api/dsh-system-monitor/snapshot?refresh=1Force a fresh hardware probe before answering
/api/dsh-system-monitor/healthPlugin identity, version, cadence — never touches hardware

snapshot returns:

{
  "ok": true,
  "version": "0.1.0",
  "ts": 1780000000000,
  "host": { "hostname": "dev-box", "platform": "win32", "arch": "x64", "uptimeSec": 3600, "pid": 42 },
  "cpu": {
    "usage": 23.4,                 // percent, null until a second sample exists
    "perCore": [10, 90, 50, 0],
    "cores": 4,
    "model": "AMD Ryzen 7 8845HS w/ Radeon 780M Graphics",
    "speedMHz": 3800,
    "temperature": 81.9,           // Celsius, null when unavailable
    "temperatureSource": "acpi-thermal-zone",
    "temperatureZones": [{ "name": "\\_SB.ECTZ", "celsius": 81.9 }]
  },
  "memory": { "totalBytes": 34359738368, "usedBytes": 20820942848, "freeBytes": 13538795520, "usage": 60.6 },
  "gpus": [{
    "index": 0,
    "name": "NVIDIA GeForce RTX 5070 Ti Laptop GPU",
    "vendor": "nvidia",
    "source": "nvidia-smi",
    "usage": 42,
    "temperature": 61,
    "powerWatts": 88.5,
    "memory": { "usedBytes": 4294967296, "totalBytes": 12884901888, "usage": 33.3 }
  }],
  "gpuSource": "nvidia-smi",
  "errors": []                     // per-metric diagnostics, capped at 6
}

Every numeric field is null rather than 0 when the reading is unavailable.

Privacy and security

  • Loopback only. Every request must arrive from a direct 127.0.0.1 / ::1 peer; anything else is 403 before a single metric is read. A request carrying a proxy's X-Forwarded-For / Forwarded header is refused too, so a reverse-proxied LAN client cannot reach the plugin by pretending to be local.
  • Read-only. There are no mutation routes. Nothing this plugin exposes can change a file, a setting, or a process — so there is no CSRF surface to defend.
  • No network egress. The plugin makes no outbound requests and sends no telemetry. It never contacts a vendor API or the internet.
  • No elevation. Nothing runs as administrator/root, no driver is installed, no system service is registered.
  • Nothing leaves the machine. Readings are served to the browser you are already looking at and are never written to disk.

The snapshot does reveal host inventory (CPU/GPU model names, memory pressure) to whatever can reach the loopback port. That is inherent to a system monitor; the fence is what keeps it local.

How each number is obtained

This plugin installs no drivers and reads no undocumented memory. Every reading comes from one of three places, and each is labelled with its source field.

CPU utilization — os.cpus() cumulative per-core jiffies, differenced between ticks. The first sample after start has no baseline, so usage is null until the second.

Memory — os.totalmem() / os.freemem(). On Windows this is GlobalMemoryStatusEx.ullAvailPhys (the "available" figure), which is why the percentage matches Task Manager rather than a raw free-page count.

CPU temperature, Windows — the PDH counter set \Thermal Zone Information(*)\Temperature, read with the built-in typeperf tool. Windows exposes no CPU die temperature to a non-elevated process (MSAcpi_ThermalZoneTemperature and the vendor WMI namespaces are refused or absent without drivers), so the ACPI thermal zones are what is available.

On the unit. The PDH counter is widely documented as "degrees Kelvin", but ACPI's underlying _TMP is deci-Kelvin — and deci-Kelvin is impossible for the values a real machine reports (355 deci-Kelvin is 35.5 K). Measured on a Windows 11 AMD laptop: the counter idles at ~355 and plateaus at ~367 under sustained 4-thread load. Read as Kelvin that is 82 °C → 94 °C, i.e. a laptop CPU settling just under its 95 °C throttle. Read as deci-Celsius the same trace would be a 1.2 °C rise under full load, which no physical package does. This plugin therefore reads Kelvin, and cpuTemperatureScale lets you override it for a source that behaves differently.

CPU temperature, Linux — /sys/class/thermal/thermal_zone*/temp and the hwmon chips whose driver name is a CPU sensor (coretemp, k10temp, zenpower, cpu_thermal, acpitz). The hottest zone is reported as the headline figure and every zone is listed in temperatureZones.

GPU, NVIDIA (Windows and Linux) — nvidia-smi --query-gpu=index,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw,name --format=csv,noheader,nounits, invoked with a fixed argument vector. name is requested last on purpose: GPU marketing names contain commas, and a trailing free-text column can be re-joined safely whereas a middle one cannot.

GPU, Windows without nvidia-smi — the vendor-neutral PDH counters \GPU Engine(*)\Utilization Percentage and \GPU Adapter Memory(*)\Dedicated Usage, grouped per adapter. Utilization is the maximum across an adapter's engine instances, not the sum: the counters are per-engine percentages, so summing a frame that is both rendered and copied would exceed 100%. This counter set carries no temperature and no VRAM capacity, so those stay null.

GPU, AMD on Linux — amdgpu sysfs: gpu_busy_percent, mem_info_vram_used / mem_info_vram_total, and the hwmon temperature nodes. No process needed.

Network throughput, Windows — the PDH counters \Network Interface(*)\Bytes Received/sec and \Bytes Sent/sec, read with the same typeperf call pattern as the thermal zones. Those counters are already rates, so no differencing is needed; the columns are counter-major (every Received instance, then every Sent one), so the direction is read from each header path rather than assumed from the column position. Only adapters passing the exclusion filter are summed.

Network throughput, Linux — /proc/net/dev, which carries cumulative byte counts. This is the one reading that is differenced rather than used directly, so the first sample after start has no interval to measure and reports —, exactly like CPU utilization.

Throughput is never a temperature-style alarm — it has no meaningful threshold, so the value never turns amber or red.

Zero is treated as "no reading" for every temperature path: ACPI reports 0 for an absent sensor and kernel drivers publish 0 for an unpopulated one. Reporting that as a genuine 0 °C would be worse than reporting nothing.

Troubleshooting

The tile never appears. The browser half needs the host half. Check GET /api/dsh-system-monitor/health in the same browser: a JSON reply means the host half is up and the problem is client-side (reload the app). A 404 means the plugin did not load — confirm the row is in the profile's cordis.patch.yml and restart dsh web.

GPU shows No GPU detected. Nothing answered. Verify nvidia-smi is on PATH (nvidia-smi --query-gpu=name --format=csv,noheader); if it lives elsewhere, set nvidiaSmiPath to the absolute path. On Windows the vendor-neutral fallback still reports utilization for AMD/Intel adapters, but without temperature or a VRAM total — that data is simply not published by the platform.

CPU temperature shows —. On Windows the ACPI thermal-zone counter is absent on some virtual machines and some older firmware. On Linux no CPU hwmon chip was found. On macOS there is no non-root source at all. Check the errors array in the snapshot for the specific reason.

The temperature looks wrong by a fixed factor. Set cpuTemperatureScale — see the unit discussion above.

网速 / NET shows — or stays at 0 B/s. 0 B/s means the counters were read and the machine is simply idle, which is correct. — means nothing could be measured: macOS has no non-root source (unsupported), or on Linux the first sample had no interval yet — the next poll resolves it. To see what was counted, read network.interfaces in the snapshot: each entry carries a counted flag. If your traffic runs over an adapter the default filter drops (a VPN, or a virtual switch), add a fragment to networkInclude.

The capsule responds slowly, or a machine feels busier. Each of gpuMs, cpuTemperatureMs and networkMs controls one short-lived helper process. Raise the cadence, or set gpu: false / cpuTemperature: false / network: false to switch a probe off entirely. CPU, memory and the CPU rate counters cost nothing measurable.

Development

npm install --ignore-scripts   # --ignore-scripts avoids esbuild's postinstall probe
npm run build                  # bundles src/client/** into lib/client.js
npm test                       # node --test test/
npm run verify                 # build, then test
npm run test:inline            # the same suites in one process
npm run dev                    # host half on :43199 + a page that boots the real tile

npm test needs the test runner to spawn a child process per file. Sandboxes and restricted environments can refuse that; npm run test:inline imports the same suites into a single process instead.

npm run dev mounts the real host half on http://127.0.0.1:43199/ and serves a page that boots the real lib/client.js with the same __ModuleLoader__ contract the shell uses — so the tile can be exercised against actual hardware without installing into a DSH profile and restarting the app. The page inlines the shell's own :root / body / body[data-ds-dark-theme] rule blocks (extracted from @deepseek-ai/dsh-client-ui-theme, overridable with DSM_THEME_CLIENT) and offers a theme toggle, so the token-following behaviour can be checked for real rather than taken on trust. It also prints the tile's rows as text on startup, which is the quickest way to read the layout without a browser. It is a development tool and is not part of the published package.

The host half (lib/index.js and lib/metrics/**) is plain ESM and needs no build. Only the browser half is bundled, because the DSH client module system wants a single classic script that calls window.__ModuleLoader__.load({ id, factory }); the build wraps esbuild's cjs output in exactly that factory and keeps react / react-dom/client external to the shell's module table.

Layout

PathWhat it is
lib/index.jsHost half — the cordis plugin entry
lib/routes.js, lib/http.js, lib/trust.jsRoutes, JSON writer, loopback fence
lib/metrics/Collectors: parse, exec, cpu, memory, cputemp, gpu, monitor
src/client/Browser half sources: model (pure logic), tile, settings, styles, locales
lib/client.jsBuilt browser half (committed so a link: install needs no build)
test/node:test suites, including a jsdom run of the built bundle
tools/Development only: dev-server.mjs, run-tests.mjs

Testing notes

test/client-bundle.test.mjs loads the built lib/client.js in jsdom through the real __ModuleLoader__ contract, with a require shim that only answers the modules the shell actually provides — so a bad external (or a stale bundle) fails the suite rather than the app. test/fixtures/ holds real typeperf and nvidia-smi output captured from a hybrid-GPU Windows laptop, so the parsers are tested against what the tools actually print, not idealized input.

Run npm run build before npm test after touching anything under src/client/.

License

The Unlicense — completely free, with no conditions on use.

This software is released into the public domain: anyone may copy, modify, publish, use, compile, sell or distribute it, in source or binary form, for any purpose, commercial or non-commercial, with no attribution, no notice to keep and no terms to comply with.

See LICENSE for the full text, or https://unlicense.org. As with any public-domain release, the software comes as-is, with no warranty.