From 96da297c9ff39d3d3aa4639ed4969a214d5345b2 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Thu, 13 Aug 2026 12:40:44 +0800 Subject: [PATCH 1/3] feat: refine UI and installer experience --- AI_HANDOFF.md | 25 ++ README.md | 4 +- installer/ThermoTray.iss | 30 +- src/ThermoTray/App.xaml | 214 +++++++--- src/ThermoTray/MainWindow.xaml | 403 +++++++++++++----- src/ThermoTray/MainWindow.xaml.cs | 73 ++++ src/ThermoTray/StringToVisibilityConverter.cs | 18 + .../InstallerContractTests.cs | 81 ++++ .../ThermoTray.Tests/MainWindowLayoutTests.cs | 59 ++- 9 files changed, 711 insertions(+), 196 deletions(-) create mode 100644 src/ThermoTray/StringToVisibilityConverter.cs create mode 100644 tests/ThermoTray.Tests/InstallerContractTests.cs diff --git a/AI_HANDOFF.md b/AI_HANDOFF.md index 0874e83..9c4a68c 100644 --- a/AI_HANDOFF.md +++ b/AI_HANDOFF.md @@ -1,5 +1,30 @@ # AI handoff +## 2026-08-13 GitHub upload (in progress) + +- User authorized uploading the complete current verified worktree, including the WPF redesign and installer-experience hardening. Scope is this repository only. +- Remote audit after `fetch --prune --tags`: local `main` and `origin/main` are identical with no other local/remote branches. Publish through `agent/ui-installer-experience`, one intentional commit, and a Draft PR targeting `main`. +- This authorization does not include a GitHub Release, tag, installer execution, or installed-application update. + +## 2026-08-13 installer experience hardening (complete, uncommitted) + +- Scope is limited to `installer/ThermoTray.iss` and a new installer contract test; the concurrent uncommitted WPF redesign remains preserved and untouched. +- The installer now always creates Start Menu application/uninstall shortcuts with a stable AppUserModelID and `{app}` working directory; the desktop shortcut remains optional and defaults unchecked. +- Added previous-install preference reuse, setup/uninstall logging, explicit uninstall identity/version fields, GitHub support/update metadata, installed README/LICENSE files, and matching bilingual README guidance for manual Start/Taskbar pinning. The existing AppMutex handover and `CloseApplications=no` safety design are preserved. +- First test compile failed only because this test project does not enable implicit `System`/`System.IO` imports and the new standalone test omitted them. The imports were added before the successful rerun; no product or installer failure was observed. +- Verification after correction: Release build passed with 0 warnings/errors, tests passed 152/152, formatting passed, self-contained publish passed, and `mt.exe` verified the embedded `requireAdministrator` manifest. Inno Setup 6.7.3 compiled the installer and the canonical release packaging regenerated `SHA256SUMS.txt`. Final installer: `artifacts/release/ThermoTray-Setup-1.1.7.exe`, 54,036,016 bytes, SHA-256 `920018D3F19E4CEAD2B617079D7CF2B2BD0F1C9275CADA4A399DBC313E862686`, file version 1.1.7. +- No installer was executed, no installed application was updated, and no commit/push/release was performed. + +## 2026-08-13 taste-skill UI redesign + +- Scope is a UI-only targeted evolution of the elevated WPF monitor. The sensor pipeline, availability rules, multi-GPU identity, polling cadence, tray rendering, and persisted per-metric tray visibility are explicitly preserved. +- Design Read: a trustworthy, compact Windows hardware instrument panel for everyday users and diagnostics. Dials are `DESIGN_VARIANCE 4`, `MOTION_INTENSITY 2`, and `VISUAL_DENSITY 8`; native WPF remains the only design system and no dependency is being added. +- Existing audit: the cool dark palette used separate mint CPU and blue GPU accents, generic rounded cards, cramped two-column CPU/GPU composition, repeated tiny tray toggles, a visually weak status line, mixed corner radii, and no explicit high-contrast palette. Information architecture is sound and remains header, live devices, contextual status/action, and preferences. +- Implemented one restrained mint accent, neutral device identity, full-width device rows with paired usage/temperature metrics, consistent 12 px panel and 8 px control radii, clearer focus/hover/pressed/disabled states, scroll-safe multi-GPU layout, contextual status panel, and runtime high-contrast resource switching. Empty status text collapses the status panel; actionable/error states remain contextual and use polite UI Automation live announcements. +- `MainWindow.xaml` keeps every existing binding and tray visibility toggle. It caps the live-device surface at 560 px with vertical scrolling, so a second or later GPU no longer makes the whole window exceed the work area. Numeric readings use the installed Consolas monospace face while interface copy uses Segoe UI Variable Text/Segoe UI. No package or font dependency was added. +- Added `StringToVisibilityConverter` and expanded the rendered-window layout contract with scroll-safety, dense minimum-width, single-accent, and radius assertions. Release build passed with 0 warnings/errors; all 150 tests passed; `dotnet format --verify-no-changes` and `git diff --check` passed. The first sandboxed build was blocked from the user's NuGet config; the host build then exposed ambiguous `SystemColors` because WinForms and WPF are both enabled, which was fixed with explicit `System.Windows.SystemColors` references before the clean build. +- Visual scope is verified through the existing STA rendered-window tests, not by an inspected desktop screenshot. The shipped app requires elevation, and no UAC/elevated live launch was performed, so actual high-contrast switching, live hardware values, tray visuals/order, and desktop-scale appearance remain unverified in this pass. + ## 2026-08-12 v1.1.7 publication and installed update - Published GitHub Release `v1.1.7` from `e222bdea567951b2e5df7a0710f08ee1f02b2d66`; branch Build and tag Release succeeded. Downloaded digests: ZIP `B36D4FB1...91E11EC`, Setup `70A42B87...5331A`. diff --git a/README.md b/README.md index 30c4747..c4b1807 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ PawnIO 基於安全性僅允許已提權(Elevated)的處理程序存取核 | 檔案類型 | 檔案名稱 | 說明 | | :--- | :--- | :--- | -| **安裝版** | `ThermoTray-Setup-x.x.x.exe` | Inno Setup 安裝精靈,安裝至使用者的 LocalAppData 目錄,支援覆蓋升級與自動偵測執行狀態。 | +| **安裝版** | `ThermoTray-Setup-x.x.x.exe` | Inno Setup 安裝精靈,安裝至使用者的 LocalAppData,支援覆蓋升級與執行狀態偵測;固定建立開始功能表的啟動/解除安裝捷徑,可自行釘選到開始或工作列,桌面捷徑則為預設不勾選的選用項目。 | | **可攜版** | `ThermoTray-x.x.x-win-x64-portable.zip` | 免安裝綠色壓縮檔,解壓縮至任意目錄即可直接執行 `ThermoTray.exe`。 | --- @@ -231,7 +231,7 @@ PawnIO requires elevated process privileges to access hardware registers. Visit the [GitHub Releases Page](https://github.com/nojackno2-ctrl/ThermoTray/releases/latest): -- **Setup Installer (`ThermoTray-Setup-x.x.x.exe`)**: Installs to `%LOCALAPPDATA%`, checks for running instances, and supports clean upgrades. +- **Setup Installer (`ThermoTray-Setup-x.x.x.exe`)**: Installs to `%LOCALAPPDATA%`, checks for running instances, supports clean upgrades, and always creates Start Menu launch/uninstall shortcuts that users can pin manually. The optional desktop shortcut is off by default. - **Portable ZIP (`ThermoTray-x.x.x-win-x64-portable.zip`)**: Extract anywhere and launch `ThermoTray.exe`. --- diff --git a/installer/ThermoTray.iss b/installer/ThermoTray.iss index 750aea2..42a138b 100644 --- a/installer/ThermoTray.iss +++ b/installer/ThermoTray.iss @@ -11,16 +11,30 @@ #define AppVersion "1.1.7" #endif #define AppPublisher "ThermoTray" +#define AppURL "https://github.com/nojackno2-ctrl/ThermoTray" #define AppExeName "ThermoTray.exe" +#define AppUserModelId "nojackno2.ThermoTray" [Setup] AppId={{1B57D245-2F65-4E07-B338-CC94F7C7E6CB} AppName={#AppName} AppVersion={#AppVersion} +AppVerName={#AppName} v{#AppVersion} AppPublisher={#AppPublisher} +AppPublisherURL={#AppURL} +AppSupportURL={#AppURL}/issues +AppUpdatesURL={#AppURL}/releases +AppReadmeFile={app}\README.md +VersionInfoVersion={#AppVersion}.0 +VersionInfoCompany={#AppPublisher} +VersionInfoDescription={#AppName} installer +VersionInfoTextVersion={#AppVersion} +VersionInfoProductVersion={#AppVersion}.0 +VersionInfoProductName={#AppName} ; 安裝至使用者 AppData\Local 目錄,不需系統管理員安裝權限 DefaultDirName={localappdata}\Programs\{#AppName} DefaultGroupName={#AppName} +AllowNoIcons=no DisableProgramGroupPage=yes OutputDir=..\artifacts\installer OutputBaseFilename=ThermoTray-Setup-{#AppVersion} @@ -30,10 +44,19 @@ ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible PrivilegesRequired=lowest UninstallDisplayIcon={app}\{#AppExeName} +UninstallDisplayName={#AppName} v{#AppVersion} +Uninstallable=yes +UsePreviousAppDir=yes +UsePreviousGroup=yes +UsePreviousTasks=yes +CreateUninstallRegKey=yes +SetupLogging=yes +UninstallLogging=yes ; 檢查應用程式產生的全域互斥鎖,避免在 ThermoTray 執行中進行覆蓋安裝 AppMutex=Global\ThermoTray.Setup ; 關閉預設的 Restart Manager,改由 AppMutex 提示使用者結束程式 CloseApplications=no +RestartApplications=no [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -41,13 +64,16 @@ Name: "english"; MessagesFile: "compiler:Default.isl" [Files] ; 包含發布目錄下的所有檔案(排除 .pdb 偵錯檔) Source: "..\publish\win-x64\*"; DestDir: "{app}"; Excludes: "*.pdb"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\LICENSE"; DestDir: "{app}"; Flags: ignoreversion [Tasks] Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: unchecked [Icons] -Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}" -Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; WorkingDir: "{app}"; AppUserModelID: "{#AppUserModelId}" +Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; WorkingDir: "{app}"; AppUserModelID: "{#AppUserModelId}"; Tasks: desktopicon [Registry] ; 舊版本 Run 登錄項清理標記 diff --git a/src/ThermoTray/App.xaml b/src/ThermoTray/App.xaml index 6170ef8..ec7d71b 100644 --- a/src/ThermoTray/App.xaml +++ b/src/ThermoTray/App.xaml @@ -1,69 +1,127 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="clr-namespace:ThermoTray"> - - - - - - - - - - + + + + #171A1F + #1D2229 + #232A33 + #2B343F + #313C49 + #3D4856 + #45C9A8 + #171A1F + #F3F6F8 + #B7C1CC + #8E9BAA + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ThermoTray/MainWindow.xaml b/src/ThermoTray/MainWindow.xaml index 0615003..0edf136 100644 --- a/src/ThermoTray/MainWindow.xaml +++ b/src/ThermoTray/MainWindow.xaml @@ -2,131 +2,308 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=PresentationFramework" - Title="ThermoTray" Width="540" MinWidth="460" - Background="{StaticResource PanelBrush}" Foreground="White" + Title="ThermoTray" + Width="720" + MinWidth="600" + MaxHeight="{Binding Source={x:Static SystemParameters.WorkArea}, Path=Height}" + SizeToContent="Height" + Background="{DynamicResource WindowBrush}" + Foreground="{DynamicResource TextPrimaryBrush}" + FontFamily="Segoe UI Variable Text, Segoe UI" + FontSize="14" Icon="ThermoTray.ico" WindowStartupLocation="CenterScreen" - SizeToContent="Height"> - - + UseLayoutRounding="True" + SnapsToDevicePixels="True" + KeyboardNavigation.TabNavigation="Cycle"> + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - 繁體中文 - English - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 繁體中文 + English + + diff --git a/src/ThermoTray/MainWindow.xaml.cs b/src/ThermoTray/MainWindow.xaml.cs index 2e5dc0e..f159462 100644 --- a/src/ThermoTray/MainWindow.xaml.cs +++ b/src/ThermoTray/MainWindow.xaml.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Windows; +using System.Windows.Media; using System.Windows.Navigation; namespace ThermoTray; @@ -10,6 +11,23 @@ namespace ThermoTray; /// public partial class MainWindow : Window { + private static readonly string[] PaletteResourceKeys = + [ + "WindowBrush", + "PanelBrush", + "SurfaceBrush", + "SurfaceHoverBrush", + "SurfacePressedBrush", + "BorderBrush", + "AccentBrush", + "AccentTextBrush", + "TextPrimaryBrush", + "TextSecondaryBrush", + "TextMutedBrush", + ]; + + private readonly Dictionary _standardPalette = new(StringComparer.Ordinal); + /// /// 標記是否為真正的關閉請求(由選單「結束」觸發),而非點擊右上角 X 按鈕隱藏至系統匣。 /// @@ -21,7 +39,62 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); + CaptureStandardPalette(); + ApplyAccessibilityPalette(); + SystemParameters.StaticPropertyChanged += OnSystemParametersStaticPropertyChanged; StateChanged += OnStateChanged; + Closed += OnClosed; + } + + private void CaptureStandardPalette() + { + foreach (var key in PaletteResourceKeys) + { + if (System.Windows.Application.Current.Resources[key] is object value) + { + _standardPalette[key] = value; + } + } + } + + private void ApplyAccessibilityPalette() + { + var resources = System.Windows.Application.Current.Resources; + if (!SystemParameters.HighContrast) + { + foreach (var pair in _standardPalette) + { + resources[pair.Key] = pair.Value; + } + + return; + } + + resources["WindowBrush"] = System.Windows.SystemColors.WindowBrush; + resources["PanelBrush"] = System.Windows.SystemColors.WindowBrush; + resources["SurfaceBrush"] = System.Windows.SystemColors.ControlBrush; + resources["SurfaceHoverBrush"] = System.Windows.SystemColors.ControlLightBrush; + resources["SurfacePressedBrush"] = System.Windows.SystemColors.ControlDarkBrush; + resources["BorderBrush"] = System.Windows.SystemColors.ActiveBorderBrush; + resources["AccentBrush"] = System.Windows.SystemColors.HighlightBrush; + resources["AccentTextBrush"] = System.Windows.SystemColors.HighlightTextBrush; + resources["TextPrimaryBrush"] = System.Windows.SystemColors.WindowTextBrush; + resources["TextSecondaryBrush"] = System.Windows.SystemColors.WindowTextBrush; + resources["TextMutedBrush"] = System.Windows.SystemColors.GrayTextBrush; + } + + private void OnSystemParametersStaticPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.Equals(e.PropertyName, nameof(SystemParameters.HighContrast), StringComparison.Ordinal)) + { + ApplyAccessibilityPalette(); + } + } + + private void OnClosed(object? sender, EventArgs e) + { + SystemParameters.StaticPropertyChanged -= OnSystemParametersStaticPropertyChanged; + Closed -= OnClosed; } /// diff --git a/src/ThermoTray/StringToVisibilityConverter.cs b/src/ThermoTray/StringToVisibilityConverter.cs new file mode 100644 index 0000000..4dc41f0 --- /dev/null +++ b/src/ThermoTray/StringToVisibilityConverter.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace ThermoTray; + +/// +/// Collapses contextual UI when its bound message is empty, while keeping non-empty status and +/// error states in the accessibility tree. +/// +public sealed class StringToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + string.IsNullOrWhiteSpace(value as string) ? Visibility.Collapsed : Visibility.Visible; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} diff --git a/tests/ThermoTray.Tests/InstallerContractTests.cs b/tests/ThermoTray.Tests/InstallerContractTests.cs new file mode 100644 index 0000000..564af9d --- /dev/null +++ b/tests/ThermoTray.Tests/InstallerContractTests.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using Xunit; + +namespace ThermoTray.Tests; + +/// +/// 驗證安裝器持續提供一致的開始功能表、升級、解除安裝與診斷體驗。 +/// +public sealed class InstallerContractTests +{ + /// + /// 驗證升級設定、應用程式與功能中繼資料,以及安裝紀錄不會被後續修改移除。 + /// + [Fact] + public void InstallerProvidesConsistentUpgradeAndDiagnosticsMetadata() + { + var script = ReadInstallerScript(); + + foreach (var directive in new[] + { + "AllowNoIcons=no", + "DisableProgramGroupPage=yes", + "UsePreviousAppDir=yes", + "UsePreviousGroup=yes", + "UsePreviousTasks=yes", + "CreateUninstallRegKey=yes", + "SetupLogging=yes", + "UninstallLogging=yes", + "CloseApplications=no", + "RestartApplications=no" + }) + { + Assert.Contains(directive, script.Lines, StringComparer.Ordinal); + } + + Assert.Contains("AppPublisherURL={#AppURL}", script.Lines, StringComparer.Ordinal); + Assert.Contains("AppSupportURL={#AppURL}/issues", script.Lines, StringComparer.Ordinal); + Assert.Contains("AppUpdatesURL={#AppURL}/releases", script.Lines, StringComparer.Ordinal); + } + + /// + /// 驗證開始功能表固定建立啟動與解除安裝捷徑,而桌面捷徑仍由使用者自行選擇。 + /// + [Fact] + public void StartMenuAndOptionalShortcutPolicyIsPreserved() + { + var script = ReadInstallerScript(); + var startMenuShortcut = Assert.Single(script.Lines, line => + line.StartsWith("Name: \"{group}\\{#AppName}\";", StringComparison.Ordinal)); + var uninstallShortcut = Assert.Single(script.Lines, line => + line.Contains("{cm:UninstallProgram,{#AppName}}", StringComparison.Ordinal)); + var desktopTask = Assert.Single(script.Lines, line => + line.StartsWith("Name: \"desktopicon\";", StringComparison.Ordinal)); + + Assert.DoesNotContain("Tasks:", startMenuShortcut, StringComparison.Ordinal); + Assert.DoesNotContain("Tasks:", uninstallShortcut, StringComparison.Ordinal); + Assert.Contains("WorkingDir: \"{app}\"", startMenuShortcut, StringComparison.Ordinal); + Assert.Contains("AppUserModelID:", startMenuShortcut, StringComparison.Ordinal); + Assert.Contains("Flags: unchecked", desktopTask, StringComparison.Ordinal); + } + + private static (string Content, string[] Lines) ReadInstallerScript() + { + var repositoryRoot = FindRepositoryRoot(); + var content = File.ReadAllText(Path.Combine(repositoryRoot, "installer", "ThermoTray.iss")); + return (content, content.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries)); + } + + private static string FindRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "ThermoTray.sln"))) + { + directory = directory.Parent; + } + + Assert.NotNull(directory); + return directory.FullName; + } +} diff --git a/tests/ThermoTray.Tests/MainWindowLayoutTests.cs b/tests/ThermoTray.Tests/MainWindowLayoutTests.cs index 2c1142a..ae226b2 100644 --- a/tests/ThermoTray.Tests/MainWindowLayoutTests.cs +++ b/tests/ThermoTray.Tests/MainWindowLayoutTests.cs @@ -24,7 +24,8 @@ public sealed class MainWindowLayoutTests : IClassFixture public void TheCardsShowTheirWholeContents() => _fixture.Invoke(window => { - AssertContentFits(window.CpuCard, "CPU"); + Assert.True(window.CpuCard.ActualHeight > 0, "the CPU panel renders nothing"); + Assert.True(window.CpuCard.DesiredSize.Height <= window.CpuCard.ActualHeight + 0.5); Assert.Equal(2, window.GpuCards.Items.Count); Assert.True(window.GpuCards.ActualHeight > 0, "the independent GPU cards render nothing"); }); @@ -33,11 +34,9 @@ public void TheCardsShowTheirWholeContents() => public void TheCpuDeviceNameIsTheFirstCardLineAndEachMetricHasATrayToggle() => _fixture.Invoke(window => { - var stack = (StackPanel)window.CpuCard.Child; - - Assert.Equal("AMD Ryzen 9 5900HS with Radeon Graphics", ((TextBlock)stack.Children[0]).Text); - Assert.IsType(((Grid)stack.Children[1]).Children[1]); - Assert.IsType(((Grid)stack.Children[3]).Children[1]); + Assert.Equal("AMD Ryzen 9 5900HS with Radeon Graphics", window.CpuDeviceNameText.Text); + Assert.True(window.CpuUsageTrayToggle.IsChecked); + Assert.True(window.CpuTemperatureTrayToggle.IsChecked); }); /// @@ -48,7 +47,7 @@ public void TheSettingsBelowTheCardsStayInTheWindow() => _fixture.Invoke(window => { var content = (FrameworkElement)window.Content; - var settings = (FrameworkElement)((Grid)content).Children[^1]; + var settings = window.SettingsPanel; var bottom = settings.TransformToAncestor(content).TransformBounds(new Rect(settings.RenderSize)).Bottom; Assert.True( @@ -71,8 +70,7 @@ public void TheHeaderShowsTheProductVersion() => _fixture.Invoke(window => { var content = (FrameworkElement)window.Content; - var title = (StackPanel)((StackPanel)((Grid)content).Children[0]).Children[0]; - var version = (TextBlock)title.Children[1]; + var version = window.ProductVersionText; Assert.Equal(MainViewModel.FormatVersion(typeof(MainViewModel).Assembly.GetName().Version), version.Text); Assert.True(version.ActualWidth > 0, "the version renders nothing"); @@ -95,6 +93,49 @@ private static void AssertContentFits(Border card, string which) bottom <= available + 0.5, $"the {which} card cuts its last line off: it reaches {bottom} inside a card that ends at {available}"); } + + [Fact] + public void TheMultiGpuSurfaceIsScrollSafeAndKeepsADenseMinimumWidth() => + _fixture.Invoke(window => + { + var scrollViewer = FindVisualChild(window.RootLayout); + + Assert.NotNull(scrollViewer); + Assert.Equal(ScrollBarVisibility.Auto, scrollViewer.VerticalScrollBarVisibility); + Assert.Equal(560, scrollViewer.MaxHeight); + Assert.Equal(600, window.MinWidth); + }); + + [Fact] + public void TheRedesignUsesOneAccentAndConsistentPanelRadii() => + _fixture.Invoke(window => + { + Assert.True(System.Windows.Application.Current.Resources.Contains("AccentBrush")); + Assert.False(System.Windows.Application.Current.Resources.Contains("GpuAccentBrush")); + Assert.Equal(new CornerRadius(12), window.CpuCard.CornerRadius); + Assert.Equal(new CornerRadius(12), window.StatusPanel.CornerRadius); + }); + + private static T? FindVisualChild(DependencyObject parent) + where T : DependencyObject + { + for (var index = 0; index < System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); index++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, index); + if (child is T match) + { + return match; + } + + var descendant = FindVisualChild(child); + if (descendant is not null) + { + return descendant; + } + } + + return null; + } } /// From 04d2db34db0cae6a71c083cdc7ce1ef0b4a3029c Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Thu, 13 Aug 2026 12:43:38 +0800 Subject: [PATCH 2/3] docs: record GitHub upload --- AI_HANDOFF.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AI_HANDOFF.md b/AI_HANDOFF.md index 9c4a68c..4b872f8 100644 --- a/AI_HANDOFF.md +++ b/AI_HANDOFF.md @@ -1,9 +1,10 @@ # AI handoff -## 2026-08-13 GitHub upload (in progress) +## 2026-08-13 GitHub upload (complete) - User authorized uploading the complete current verified worktree, including the WPF redesign and installer-experience hardening. Scope is this repository only. -- Remote audit after `fetch --prune --tags`: local `main` and `origin/main` are identical with no other local/remote branches. Publish through `agent/ui-installer-experience`, one intentional commit, and a Draft PR targeting `main`. +- Remote audit after `fetch --prune --tags` found local `main` and `origin/main` identical with no other pre-existing branches. Initial source commit `96da297` was pushed to `agent/ui-installer-experience`; Draft PR #6 targets `main`: https://github.com/nojackno2-ctrl/ThermoTray/pull/6 +- GitHub connector PR creation returned `403 Resource not accessible by integration`; the authenticated host `gh pr create` fallback succeeded. A final handoff-only commit records the completed remote state on the same PR branch. - This authorization does not include a GitHub Release, tag, installer execution, or installed-application update. ## 2026-08-13 installer experience hardening (complete, uncommitted) From 952669f604bf2f9bcf23c8d924e838dadbd0acbd Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Fri, 14 Aug 2026 18:37:35 +0800 Subject: [PATCH 3/3] fix(tests): resolve Application.Current resource lookup in MainWindowLayoutTests --- AI_HANDOFF.md | 8 ++++++++ tests/ThermoTray.Tests/MainWindowLayoutTests.cs | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/AI_HANDOFF.md b/AI_HANDOFF.md index 4b872f8..1761729 100644 --- a/AI_HANDOFF.md +++ b/AI_HANDOFF.md @@ -375,4 +375,12 @@ Findings and what each one changed. None of them reproduced on this machine's ow - Corrected pre-commit assets: portable ZIP contains 8 expected files and no stale running executable; ZIP SHA-256 `47F5653BE7CAE19BB9E0800591A440420665186707A2916FE0ADE2BA4A8B5DEB`; Setup SHA-256 `6823B36DA9665E25365B12E97612F7C3B9DAC5A9307D29E083D94AB9FB57361C`. - Notification-area ordering and fresh elevated hardware/UI behavior were not revalidated in this pass; prior v1.1.5 hardware evidence remains the latest live proof. +## 2026-08-13 installed software update verification + +- Silently updated the current-user installation with `artifacts\release\ThermoTray-Setup-1.1.7.exe`; the installer returned exit code `0` and did not request a reboot. +- Installed executable: `%LOCALAPPDATA%\Programs\ThermoTray\ThermoTray.exe`; FileVersion `1.1.7.0`, ProductVersion `1.1.7+acef9fb0781f2d6a7f9a4c995aba7843efad56a8`, SHA-256 `9ED1C1773A93415184B07772FFCB1CC025A4A51F6D9EEE6668A047645B663D91`. It is byte-for-byte identical to the current publish output. +- Verified the registered version, install location, uninstall command, project/issues/update URLs, and the Start Menu launch/uninstall shortcuts. Removed the stale duplicate root-level `ThermoTray.lnk`; the current program-group shortcut remains valid. +- Launched the exact installed executable with elevation; PID `39000` reported `Responding=True`. This verifies elevated launch and process responsiveness only; sensor correctness and fresh visual behavior were not revalidated. +- The user's authorization in this milestone covers updating installed software; this handoff entry is not yet committed or pushed. + diff --git a/tests/ThermoTray.Tests/MainWindowLayoutTests.cs b/tests/ThermoTray.Tests/MainWindowLayoutTests.cs index ae226b2..05a15bb 100644 --- a/tests/ThermoTray.Tests/MainWindowLayoutTests.cs +++ b/tests/ThermoTray.Tests/MainWindowLayoutTests.cs @@ -110,8 +110,8 @@ public void TheMultiGpuSurfaceIsScrollSafeAndKeepsADenseMinimumWidth() => public void TheRedesignUsesOneAccentAndConsistentPanelRadii() => _fixture.Invoke(window => { - Assert.True(System.Windows.Application.Current.Resources.Contains("AccentBrush")); - Assert.False(System.Windows.Application.Current.Resources.Contains("GpuAccentBrush")); + Assert.True(_fixture.Application.Resources.Contains("AccentBrush")); + Assert.False(_fixture.Application.Resources.Contains("GpuAccentBrush")); Assert.Equal(new CornerRadius(12), window.CpuCard.CornerRadius); Assert.Equal(new CornerRadius(12), window.StatusPanel.CornerRadius); }); @@ -199,6 +199,8 @@ public MainWindowFixture() _application = application!; } + public App Application => _application; + /// /// 在 UI 執行緒分派執行斷言。 ///