ccdeck 3.38.4 → 3.38.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,8 +64,8 @@
64
64
  stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
65
65
  })();
66
66
  </script>
67
- <script type="module" crossorigin src="/assets/index-FnWpX0X5.js"></script>
68
- <link rel="stylesheet" crossorigin href="/assets/index-6_mJZ_aP.css">
67
+ <script type="module" crossorigin src="/assets/index-BKLXyjIh.js"></script>
68
+ <link rel="stylesheet" crossorigin href="/assets/index-DnJPDhs9.css">
69
69
  </head>
70
70
  <body>
71
71
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccdeck",
3
- "version": "3.38.4",
3
+ "version": "3.38.6",
4
4
  "description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -72,7 +72,18 @@
72
72
  "3.37.0",
73
73
  "3.38.0",
74
74
  "3.38.1",
75
- "3.38.2"
75
+ "3.38.2",
76
+ "3.38.6"
77
+ ],
78
+ "3.38.5": [
79
+ {
80
+ "title": "🔧 Settings are in one place now",
81
+ "body": "The theme, the sounds, notifications, Claude FM and the minimap character are all in Settings, behind the gear at the right of the topbar, or Cmd+, (Ctrl+, on Windows and Linux). The speaker beside it keeps the quick things one click away: sounds on or off, and how loud each tone is. Nothing you had set has changed, it has only moved, and T still switches the theme. The auto-switch threshold also takes a custom value now: pick Custom… and type a whole percent from 50 to 99."
82
+ },
83
+ {
84
+ "title": "Windows: no more temperature stuck at 28 °C",
85
+ "body": "On many Windows laptops the only temperature Windows offers is a thermal zone wired to nothing, which reads a fixed 28 °C however hot the CPU is, and the machine panel showed it as a reading. A thermal zone is now drawn only once its value has changed. Until then the panel shows LibreHardwareMonitor's readings if its web server is running, and otherwise no Thermal section at all: the deck never invents a reading."
86
+ }
76
87
  ],
77
88
  "3.38.4": [
78
89
  {
@@ -111,10 +111,19 @@ import { run } from "./metrics-run.mjs";
111
111
  // Reasoned from the documented layout, not yet run on a localised
112
112
  // Windows.
113
113
  //
114
- // A MODERN INTEL LAPTOP declares no zone at all: Intel DTT manages
115
- // its sensors and publishes them only to an administrator. There the
116
- // degrees come from LibreHardwareMonitor's local web server when the
117
- // user already runs it — see hwmonitor.mjs.
114
+ // A ZONE IS NOT THE CPU, AND IT MAY NOT BE A SENSOR AT ALL. An ACPI
115
+ // zone measures whatever the firmware wired it to — the skin, the
116
+ // board, the embedded controller's idea of the hottest of those —
117
+ // and on a lot of laptops it measures nothing: Intel's reference
118
+ // firmware returns a constant 3010 (drawn as 28°C) when it has no
119
+ // source. So a zone is drawn only once it has been seen to change,
120
+ // and never under the name CPU. See zonesSeenAfter.
121
+ //
122
+ // A MODERN INTEL LAPTOP declares no zone at all, or one that never
123
+ // moves: Intel DTT manages its sensors and publishes them only to an
124
+ // administrator. There the degrees come from LibreHardwareMonitor's
125
+ // local web server when the user already runs it — see
126
+ // hwmonitor.mjs.
118
127
  //
119
128
  // NEVER INVENT A READING. No sensor means no row, and no rows at all means the
120
129
  // section is not rendered: not 0°C, not a dash, not a grey empty bar. Same rule
@@ -599,31 +608,93 @@ export async function readThermal(platform = process.platform, since = null) {
599
608
  return darwinThermal({ gpuC, throttle, macmon });
600
609
  }
601
610
 
602
- if (platform === "win32") {
603
- // One child for both sources rather than two, because the cost here is the
604
- // PowerShell start and not the queries: the counter is tried first because
605
- // it needs no administrator, and MSAcpi only when the counter said nothing.
606
- // Both are wrapped in their own try — "no thermal zone on this machine" is
607
- // the ordinary answer and arrives as a throw from either.
608
- const out = await run("powershell.exe", [
609
- "-NoProfile", "-NonInteractive", "-Command", WIN_THERMAL_PS,
610
- ], 6_000);
611
- const answer = out ? parseWinThermal(out.trim()) : [];
612
- if (answer.length) return { celsius: answer, throttle: null };
613
-
614
- // Windows itself had nothing, which on a modern Intel laptop is every time:
615
- // the firmware declares no ACPI thermal zone and the sensors sit behind
616
- // Intel DTT, which an ordinary process may not read. If something on this
617
- // machine has already gone and got them — LibreHardwareMonitor, with its
618
- // web server on — they are a plain HTTP read away. Never installed, never
619
- // asked for. See hwmonitor.mjs.
620
- const { readHwMonitorTemps } = await import("./hwmonitor.mjs");
621
- const t = await readHwMonitorTemps();
622
- const rows = [];
623
- if (t.cpu != null) rows.push({ label: "CPU", celsius: t.cpu, warnAt: WARN_C, critAt: CRIT_C });
624
- if (t.gpu != null) rows.push({ label: "GPU", celsius: t.gpu, warnAt: WARN_C, critAt: CRIT_C });
625
- return rows.length ? { celsius: rows, throttle: null } : null;
626
- }
611
+ if (platform === "win32") return readWindowsThermal(since);
627
612
 
628
613
  return null;
629
614
  }
615
+
616
+ /**
617
+ * The Windows rows, from the PowerShell child and, failing that, from a running
618
+ * LibreHardwareMonitor.
619
+ *
620
+ * `deps` is a seam for the reason sampleThermal's `deps.read` is: the suite
621
+ * must never start PowerShell, so `powershell` stands in for the child's output
622
+ * and `hwMonitor` for the read of the local web server.
623
+ */
624
+ export async function readWindowsThermal(since = null, deps = {}) {
625
+ // One child for both sources rather than two, because the cost here is the
626
+ // PowerShell start and not the queries: the counter is tried first because
627
+ // it needs no administrator, and MSAcpi only when the counter said nothing.
628
+ // Both are wrapped in their own try — "no thermal zone on this machine" is
629
+ // the ordinary answer and arrives as a throw from either.
630
+ const powershell = deps.powershell ?? (() => run("powershell.exe", [
631
+ "-NoProfile", "-NonInteractive", "-Command", WIN_THERMAL_PS,
632
+ ], 6_000));
633
+ const hwMonitor = deps.hwMonitor
634
+ ?? (async () => (await import("./hwmonitor.mjs")).readHwMonitorTemps());
635
+
636
+ const out = await powershell();
637
+ const zones = out ? parseWinThermal(out.trim()) : [];
638
+ const zonesSeen = zonesSeenAfter(zones, since?.zonesSeen);
639
+ const memory = Object.keys(zonesSeen).length ? { zonesSeen } : {};
640
+ const moved = zones.filter(z => zonesSeen[z.label].moved);
641
+ if (moved.length) return { celsius: moved, throttle: null, ...memory };
642
+
643
+ // Windows had no zone that has moved, which on a modern Intel laptop is
644
+ // every time: the firmware declares no ACPI thermal zone, or declares one
645
+ // that returns a constant, and the sensors sit behind Intel DTT, which an
646
+ // ordinary process may not read. If something on this machine has already
647
+ // gone and got them — LibreHardwareMonitor, with its web server on — they
648
+ // are a plain HTTP read away. Never installed, never asked for. See
649
+ // hwmonitor.mjs.
650
+ const t = await hwMonitor();
651
+ const rows = [];
652
+ if (t.cpu != null) rows.push({ label: "CPU", celsius: t.cpu, warnAt: WARN_C, critAt: CRIT_C });
653
+ if (t.gpu != null) rows.push({ label: "GPU", celsius: t.gpu, warnAt: WARN_C, critAt: CRIT_C });
654
+ if (rows.length) return { celsius: rows, throttle: null, ...memory };
655
+
656
+ // A zone that has not moved yet is still a zone, so this is an answer with
657
+ // nothing to draw rather than null. Null would let the sampler give up after
658
+ // three readings, and a real zone that idled on one value for half a minute
659
+ // would then never be drawn. thermalSnapshot keeps it off the panel.
660
+ return zones.length ? { celsius: [], throttle: null, zonesSeen } : null;
661
+ }
662
+
663
+ /**
664
+ * Which Windows zones have been seen to change since the deck started, keyed by
665
+ * the zone's row label: the first value each one read, and whether it has
666
+ * since read anything else.
667
+ *
668
+ * A ZONE IS DRAWN ONLY ONCE IT HAS MOVED. Intel's client reference firmware,
669
+ * which most laptop BIOSes are built from, ends the package zone's `_TMP` with
670
+ * "Return a static value if no source is available. Return(3010)" — 27.85°C,
671
+ * drawn as 28 — and returns a static 3000 when legacy thermal management is
672
+ * off. Where the zone does have a source it is the embedded controller's "max
673
+ * platform temperature", never the die. Drawn as it came, a zone stuck on
674
+ * 3010 reads "Thermal zone 28 °C" on a laptop whose CPU is far hotter, which is
675
+ * a reading this module invents by passing it on.
676
+ *
677
+ * The test is Microsoft's own for a zone: "Each thermal zone must report
678
+ * actual temperature on the sensor (_TMP). The test runs a varying workload on
679
+ * the PC, and the temperature is expected to change." Movement rather than a
680
+ * list of known constants, because a constant is a firmware's choice and the
681
+ * next one may choose another — and because a firmware that adds 2730 rather
682
+ * than 2732 reads a real 28°C as 3010, which a list would hide.
683
+ *
684
+ * WHAT IT COSTS: a real zone that holds one value from the moment the deck
685
+ * starts is not drawn until it first changes. Once it has, it stays drawn for
686
+ * the life of the process however still it then sits — `moved` never goes back.
687
+ * Compared in whole degrees, because that is what the row shows: a zone whose
688
+ * row would never change is indistinguishable from a constant to whoever reads
689
+ * it.
690
+ */
691
+ function zonesSeenAfter(zones, before = {}) {
692
+ const seen = { ...before };
693
+ for (const z of zones) {
694
+ const prior = seen[z.label];
695
+ seen[z.label] = prior
696
+ ? { first: prior.first, moved: prior.moved || z.celsius !== prior.first }
697
+ : { first: z.celsius, moved: false };
698
+ }
699
+ return seen;
700
+ }
@@ -181,12 +181,16 @@ export function lastThermal() {
181
181
  }
182
182
 
183
183
  /** The thermal row /api/system answers: the last reading, with whether the
184
- * machine has been held back since the deck started, or null without one. */
184
+ * machine has been held back since the deck started, or null without one —
185
+ * and null for a reading with nothing to draw, which is a Windows machine
186
+ * whose zones have not moved yet (readWindowsThermal). The panel draws a
187
+ * section for any object it is handed, and an empty one is not a reading. */
185
188
  export function thermalSnapshot() {
186
189
  if (!thermal) return null;
187
- // The raw kernel count is the sampler's, for the next difference, and not a
188
- // reading: the route answers what the panel draws.
189
- const { throttleCount, ...shown } = thermal;
190
+ // The raw kernel count and the zones' first values are the sampler's, for
191
+ // the next reading, and not readings: the route answers what the panel draws.
192
+ const { throttleCount, zonesSeen, ...shown } = thermal;
193
+ if (!shown.celsius?.length && !shown.throttle) return null;
190
194
  return { ...shown, heldBack: heldBackSoFar() };
191
195
  }
192
196