Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion AI_HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,25 @@ Findings and what each one changed. None of them reproduced on this machine's ow
- Pre-release validation on `main` at `c62a44d` (win-x64, Release): `dotnet build` 0 warnings / 0 errors, `dotnet test` 141/141 passed, `dotnet format --verify-no-changes` clean, and the self-contained single-file `dotnet publish` produced `publish/win-x64/ThermoTray.exe`.
- Fixed a tray-icon defect introduced by the multi-GPU refactor: `TrayIconService.RebuildGpuIcons` re-registers the CPU `NotifyIcon` to keep the CPU-left ordering but left `_cpuIconKey` set, so the following `UpdateCpuIcon` treated the icon as unchanged and never drew onto the fresh `NotifyIcon`. The CPU tray slot went blank the first time a GPU appeared, and stayed blank whenever the CPU digits did not change afterwards (for example when no CPU sensor is readable). `_cpuIconKey` is now cleared alongside the icon replacement.
- Bumped `<Version>` to `1.1.5` in `Directory.Build.props`, with the documentation fallbacks in `installer/ThermoTray.iss` and `README.md` updated to match.
- Elevated live verification of the tray fix on real hardware is still pending; it needs a manual launch of the published build with a GPU present.
- Merged as PR #4 into `main` at `480bcbd`, tagged `v1.1.5`, and the Release workflow published `ThermoTray-Setup-1.1.5.exe`, `ThermoTray-1.1.5-win-x64-portable.zip`, and `SHA256SUMS.txt`. Every workflow step passed, including the embedded `requireAdministrator` manifest check.

## 2026-08-05 version 1.1.5 live verification

- The user ran the released 1.1.5 build elevated on a dual-GPU laptop (Ryzen 9 5900HS with Radeon Graphics as GPU 0, GeForce RTX 3060 Laptop as GPU 1) and supplied a screenshot.
- The tray fix is confirmed on real hardware: the CPU icon rendered its digits (7 / 57) instead of the blank slot the stale `_cpuIconKey` used to leave behind. This machine exercises the exact failing path, because a GPU list appearing makes `RebuildGpuIcons` re-register the CPU `NotifyIcon`.
- Per-GPU separation held: each GPU had its own card with its own device name, the AMD integrated GPU reported `4%` utilization with an unavailable temperature, and the NVIDIA GPU reported `0%` and `44 °C` with no cross-pairing between the two.
- The per-card tray checkboxes worked: GPU 0 had both cleared and its tray icon was hidden, leaving only the CPU and GPU 1 icons in the notification area.
- Observed follow-up, not yet addressed: `GetAvailabilityMessage` returns `GpuSensorUnavailable` when **any** GPU lacks a temperature, so the status bar showed "GPU 溫度感測器未提供可信的即時讀值。" permanently even though the discrete GPU reported a valid 44 °C. On dual-GPU laptops the integrated GPU commonly has no temperature sensor, making the warning persistent and unactionable, and it does not say which GPU it refers to.
- Observed follow-up, not yet addressed: in the screenshot the CPU icon sat to the right of the GPU icon rather than the intended CPU-left default. Windows preserves manually rearranged notification-area positions and this machine has carried ThermoTray across several versions, so this may be remembered ordering rather than a defect in `restoreCpuOrder`; it needs a clean notification-area state to tell the two apart.

## 2026-08-05 version 1.1.6

- Addressed the first follow-up from the 1.1.5 verification. The user chose the behavior: name the affected GPUs, and warn only when every GPU is missing the reading.
- `MainViewModel.ListGpusMissingReading` is the new rule. It returns an empty string as soon as any GPU still reports the reading, so a dual-GPU laptop whose integrated GPU permanently lacks a temperature sensor no longer carries a standing warning while the discrete GPU reports a valid one. When every GPU is missing it, the GPUs are listed by the same `GPU 0` / `GPU 1` numbers the cards use.
- `GpuSensorUnavailable` and `GpuUsageUnavailable` are now `{0}` format strings, filled through `string.Format(CultureInfo.CurrentCulture, ...)`. Added a `ListSeparator` key so the list joins with `、` in Traditional Chinese and `, ` in English.
- Utilization follows the identical rule, so an integrated GPU that reports no utilization is silent while another GPU still reports it.
- The cards are deliberately unchanged: a GPU without a reading still shows `無法取得` in its own card, which is where the per-device detail belongs. Only the single-line status bar changed.
- Added 5 regression tests covering the mixed-availability, all-missing, single-GPU, no-GPU, and utilization cases, bringing the local total to 146.
- Validation on win-x64 Release: `dotnet build` 0 warnings / 0 errors, `dotnet test` 146/146 passed, `dotnet format --verify-no-changes` clean.
- Bumped `<Version>` to `1.1.6`, with `installer/ThermoTray.iss` and `README.md` version examples updated to match.
- The notification-area ordering follow-up from 1.1.5 is still open and was not touched by this release.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- Single source of truth for the product version. The installer and CI read it from here. -->
<Version>1.1.5</Version>
<Version>1.1.6</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ dotnet publish .\src\ThermoTray\ThermoTray.csproj -c Release -r win-x64 --self-c

### 建立安裝檔

安裝 [Inno Setup](https://jrsoftware.org/isinfo.php),以 Inno Setup Compiler 開啟 `installer\ThermoTray.iss` 並編譯(命令列可用 `ISCC.exe /DAppVersion=1.1.5 installer\ThermoTray.iss` 指定版本)。它會使用 `publish\win-x64` 的輸出,安裝檔生成於 `artifacts\installer`。安裝精靈安裝到目前使用者的 LocalAppData,本身不需系統管理員權限;啟動 ThermoTray 時才會顯示 UAC。登入自動啟動請在程式內勾選「隨 Windows 啟動」。安裝精靈使用英文;已安裝的 ThermoTray 本身可切換繁體中文與英文。
安裝 [Inno Setup](https://jrsoftware.org/isinfo.php),以 Inno Setup Compiler 開啟 `installer\ThermoTray.iss` 並編譯(命令列可用 `ISCC.exe /DAppVersion=1.1.6 installer\ThermoTray.iss` 指定版本)。它會使用 `publish\win-x64` 的輸出,安裝檔生成於 `artifacts\installer`。安裝精靈安裝到目前使用者的 LocalAppData,本身不需系統管理員權限;啟動 ThermoTray 時才會顯示 UAC。登入自動啟動請在程式內勾選「隨 Windows 啟動」。安裝精靈使用英文;已安裝的 ThermoTray 本身可切換繁體中文與英文。

### 溫度正確性說明

Expand Down Expand Up @@ -91,4 +91,4 @@ Build it with Visual Studio 2022 / .NET 8 using `ThermoTray.sln` and run `dotnet

After installation, accept UAC and wait one or two seconds for the first sample. CPU utilization and temperature plus one card per GPU are shown together; 0% is a valid idle reading, while missing data is shown as `Unavailable` and never as `0 °C` for temperature. Each card independently controls whether its utilization and temperature appear in the tray; the card values remain visible, while the icon and tooltip follow those choices. Each tray icon places utilization above temperature, and each GPU tooltip identifies its GPU number and device name. The CPU tray icon is registered to the left of the GPU icons unless Windows has preserved a manually rearranged notification-area order. The startup option creates a `ThermoTray` logon task with `RL HIGHEST` so it can start elevated without another UAC prompt.

For a tagged GitHub release, push a tag matching the version in `Directory.Build.props`, such as `v1.1.5`. The release workflow verifies the tag, publishes the self-contained `win-x64` build, creates the Inno Setup installer, creates a PDB-free portable ZIP, verifies the embedded `requireAdministrator` manifest, and publishes SHA-256 checksums with the release assets.
For a tagged GitHub release, push a tag matching the version in `Directory.Build.props`, such as `v1.1.6`. The release workflow verifies the tag, publishes the self-contained `win-x64` build, creates the Inno Setup installer, creates a PDB-free portable ZIP, verifies the embedded `requireAdministrator` manifest, and publishes SHA-256 checksums with the release assets.
4 changes: 2 additions & 2 deletions installer/ThermoTray.iss
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
; 1. 先執行自包含發行發布 (Self-contained Publish):
; dotnet publish .\src\ThermoTray\ThermoTray.csproj -c Release -r win-x64 --self-contained true -o .\publish\win-x64
; 2. 呼叫 ISCC 進行編譯:
; ISCC.exe /DAppVersion=1.1.5 .\installer\ThermoTray.iss
; ISCC.exe /DAppVersion=1.1.6 .\installer\ThermoTray.iss

#define AppName "ThermoTray"
; 可由 CI 或命令列參數 /DAppVersion=<version> 覆蓋預設版本號
#ifndef AppVersion
#define AppVersion "1.1.5"
#define AppVersion "1.1.6"
#endif
#define AppPublisher "ThermoTray"
#define AppExeName "ThermoTray.exe"
Expand Down
9 changes: 7 additions & 2 deletions src/ThermoTray/Localizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ public sealed class Localizer
"CPU 溫度需要 PawnIO 核心驅動程式。LibreHardwareMonitor 0.9.6 以 PawnIO 讀取 CPU 暫存器,未安裝時 AMD Ryzen 的 Tctl/Tdie 會固定為 0,因此顯示為無法取得。安裝 PawnIO 後重新啟動 ThermoTray 即可。",
"CPU temperature needs the PawnIO kernel driver. LibreHardwareMonitor 0.9.6 reads CPU registers through PawnIO; without it the AMD Ryzen Tctl/Tdie sensor stays at 0, so it is reported as unavailable. Install PawnIO and restart ThermoTray."),
["DriverDownload"] = ("下載並安裝 PawnIO", "Download and install PawnIO"),
["GpuSensorUnavailable"] = ("GPU 溫度感測器未提供可信的即時讀值。", "The GPU temperature sensor did not provide a trustworthy live reading."),
["GpuSensorUnavailable"] = (
"{0} 的溫度感測器未提供可信的即時讀值。",
"The temperature sensor on {0} did not provide a trustworthy live reading."),
["ListSeparator"] = ("、", ", "),
["CpuUsageUnavailable"] = ("CPU 使用率感測器未提供可信的即時讀值。", "The CPU utilization sensor did not provide a trustworthy live reading."),
["GpuUsageUnavailable"] = ("GPU 使用率感測器未提供可信的即時讀值。", "The GPU utilization sensor did not provide a trustworthy live reading."),
["GpuUsageUnavailable"] = (
"{0} 的使用率感測器未提供可信的即時讀值。",
"The utilization sensor on {0} did not provide a trustworthy live reading."),
["StartWithWindows"] = ("隨 Windows 啟動", "Start with Windows"),
["HideWhenClosed"] = ("關閉時縮小至系統匣", "Hide to tray when closed"),
["Language"] = ("語言", "Language"),
Expand Down
51 changes: 43 additions & 8 deletions src/ThermoTray/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Threading;
Expand Down Expand Up @@ -620,20 +621,54 @@ private string GetAvailabilityMessage(HardwareSnapshot snapshot)
return string.Empty;
}

foreach (var gpu in snapshot.Gpus)
var separator = T["ListSeparator"];
var withoutTemperature = ListGpusMissingReading(snapshot.Gpus, static gpu => gpu.Temperature.IsAvailable, separator);
if (withoutTemperature.Length > 0)
{
if (!gpu.Temperature.IsAvailable)
{
return T["GpuSensorUnavailable"];
}
return string.Format(CultureInfo.CurrentCulture, T["GpuSensorUnavailable"], withoutTemperature);
}

var withoutUsage = ListGpusMissingReading(snapshot.Gpus, static gpu => gpu.Usage.IsAvailable, separator);
return withoutUsage.Length > 0
? string.Format(CultureInfo.CurrentCulture, T["GpuUsageUnavailable"], withoutUsage)
: string.Empty;
}

/// <summary>
/// 列出缺少指定讀值的 GPU 序號,但只在每一張 GPU 都缺少它時才回報。
/// 雙 GPU 筆電的內顯經常永遠不提供溫度,只要還有一張 GPU 給出可信讀值,
/// 狀態列就不該常駐一則使用者無法處理、也沒指出對象的警告;
/// 卡片本身仍會以「無法取得」標示那一張缺少讀值的 GPU。
/// </summary>
/// <param name="readings">目前快照中的所有 GPU。</param>
/// <param name="isAvailable">判斷單張 GPU 是否具備該讀值的條件。</param>
/// <param name="separator">本地化的清單分隔字元。</param>
/// <returns>需要提醒的 GPU 序號清單;若任一張仍有讀值則為空字串。</returns>
internal static string ListGpusMissingReading(
IReadOnlyList<GpuReading> readings,
Func<GpuReading, bool> isAvailable,
string separator)
{
if (readings.Count == 0)
{
return string.Empty;
}

if (!gpu.Usage.IsAvailable)
foreach (var reading in readings)
{
if (isAvailable(reading))
{
return T["GpuUsageUnavailable"];
return string.Empty;
}
}

return string.Empty;
var names = new string[readings.Count];
for (var index = 0; index < readings.Count; index++)
{
names[index] = $"GPU {index}";
}

return string.Join(separator, names);
}

/// <summary>
Expand Down
69 changes: 69 additions & 0 deletions tests/ThermoTray.Tests/TrayDisplaySettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,73 @@ public void GpuSourceShowsOnlyTheSensorNameWhenDeviceNameIsAlreadyDisplayed()

Assert.Equal("GPU Core", gpu.Source);
}

/// <summary>
/// 雙 GPU 筆電的內顯常態沒有溫度感測器;只要獨顯仍給出溫度,狀態列就不該警告。
/// </summary>
[Fact]
public void NoTemperatureWarningWhileAnotherGpuStillReportsOne()
{
var readings = new[]
{
Gpu("gpu-0", temperature: null, usage: 4),
Gpu("gpu-1", temperature: 44, usage: 0),
};

Assert.Empty(MainViewModel.ListGpusMissingReading(readings, gpu => gpu.Temperature.IsAvailable, "、"));
}

/// <summary>
/// 所有 GPU 都讀不到溫度時才警告,並指出是哪幾張。
/// </summary>
[Fact]
public void EveryGpuMissingTheReadingIsListedByItsCardNumber()
{
var readings = new[]
{
Gpu("gpu-0", temperature: null, usage: 4),
Gpu("gpu-1", temperature: null, usage: 0),
};

Assert.Equal("GPU 0、GPU 1", MainViewModel.ListGpusMissingReading(readings, gpu => gpu.Temperature.IsAvailable, "、"));
}

/// <summary>
/// 單張 GPU 缺少讀值時仍會回報,序號與卡片標題一致。
/// </summary>
[Fact]
public void ASingleGpuMissingTheReadingIsReported()
{
var readings = new[] { Gpu("gpu-0", temperature: null, usage: 4) };

Assert.Equal("GPU 0", MainViewModel.ListGpusMissingReading(readings, gpu => gpu.Temperature.IsAvailable, "、"));
}

/// <summary>
/// 沒有任何 GPU 時不該組出警告文字。
/// </summary>
[Fact]
public void NoWarningWithoutAnyGpu() =>
Assert.Empty(MainViewModel.ListGpusMissingReading([], gpu => gpu.Temperature.IsAvailable, "、"));

/// <summary>
/// 使用率採用同一條規則:內顯沒有使用率時,獨顯的可信讀值就足以取消警告。
/// </summary>
[Fact]
public void TheSameRuleAppliesToUtilization()
{
var readings = new[]
{
Gpu("gpu-0", temperature: 47, usage: null),
Gpu("gpu-1", temperature: 44, usage: 0),
};

Assert.Empty(MainViewModel.ListGpusMissingReading(readings, gpu => gpu.Usage.IsAvailable, "、"));
}

private static GpuReading Gpu(string id, decimal? temperature, decimal? usage) => new(
id,
id,
temperature is decimal celsius ? new TemperatureReading(celsius, $"{id} • GPU Core") : TemperatureReading.Unavailable,
usage is decimal percent ? new UtilizationReading(percent, $"{id} • GPU Core") : UtilizationReading.Unavailable);
}