From 2eeec6f5f6d34bcbf7c6586bdd7ce49446a781dd Mon Sep 17 00:00:00 2001 From: Mathijs Beemsterboer <15211332+TheBeems@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:37:52 +0200 Subject: [PATCH 1/2] Fix stale Dock notifications and prepare 0.8.6 --- CHANGELOG.md | 6 ++ CodexUsageDock.Tests/DockNotificationTests.cs | 73 +++++++++++++++++++ CodexUsageDock.Tests/UsageDataTests.cs | 2 +- CodexUsageDock/CodexUsageDock.csproj | 2 +- CodexUsageDock/UsageDockListItem.cs | 44 ++++++++++- DEVELOPMENT.md | 5 ++ 6 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 CodexUsageDock.Tests/DockNotificationTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index a62d1dc..b062b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ the [changelog workflow](AGENTS.md#documentation-and-change-quality). ## [Unreleased] +## [0.8.6] - 2026-10-04 + +### Fixed + +- Keep Dock percentages updating when an old Command Palette notification subscriber disconnects. Deliver display changes independently to each subscriber, remove permanently disconnected subscribers, and retain transient failures for the next refresh. + ### Changed - Clarify forecast retention and the privacy-policy date, document Store updates when WinGet cannot determine a version, and correct the chart description. ([commit 533695b](https://github.com/TheBeems/CodexUsageDock/commit/533695bfee3661dc98efffd7f58844cf433ed09a)) diff --git a/CodexUsageDock.Tests/DockNotificationTests.cs b/CodexUsageDock.Tests/DockNotificationTests.cs new file mode 100644 index 0000000..0b6f9d4 --- /dev/null +++ b/CodexUsageDock.Tests/DockNotificationTests.cs @@ -0,0 +1,73 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Xunit; + +namespace CodexUsageDock.Tests; + +[SuppressMessage("Performance", "CA1859:Use concrete types when possible", + Justification = "These tests exercise host subscriptions through the public WinRT interface.")] +public sealed class DockNotificationTests +{ + [Theory] + [InlineData(unchecked((int)0x80010108))] + [InlineData(unchecked((int)0x800706BA))] + public void DisconnectedSubscriberDoesNotBlockCurrentHost(int hresult) + { + var item = new UsageDockListItem(new NoOpCommand()); + IListItem hostItem = item; + var disconnectedCalls = 0; + var displayedTitle = string.Empty; + hostItem.PropChanged += (_, _) => + { + disconnectedCalls++; + throw Marshal.GetExceptionForHR(hresult)!; + }; + hostItem.PropChanged += (_, args) => + { + if (args.PropertyName == nameof(ICommandItem.Title)) displayedTitle = hostItem.Title; + }; + + item.Title = "Week 93%"; + Assert.Equal("Week 93%", displayedTitle); + item.Title = "Week 86%"; + item.NotifyDisplayPropertiesChanged(); + + Assert.Equal("Week 86%", displayedTitle); + Assert.Equal(1, disconnectedCalls); + } + + [Fact] + public void TransientSubscriberFailureAllowsOtherNotificationsAndRecovery() + { + var item = new UsageDockListItem(new NoOpCommand()); + IListItem hostItem = item; + var fail = true; + var recoveredTitle = string.Empty; + var properties = new List(); + hostItem.PropChanged += (_, args) => + { + if (fail) throw Marshal.GetExceptionForHR(unchecked((int)0x8001010D))!; + if (args.PropertyName == nameof(ICommandItem.Title)) recoveredTitle = hostItem.Title; + }; + hostItem.PropChanged += (_, args) => properties.Add(args.PropertyName); + + item.Title = "Week 86%"; + item.NotifyDisplayPropertiesChanged(); + Assert.Contains(nameof(ICommandItem.Title), properties); + Assert.Contains(nameof(ICommandItem.Subtitle), properties); + Assert.Contains(nameof(ICommandItem.Icon), properties); + + fail = false; + item.NotifyDisplayPropertiesChanged(); + Assert.Equal("Week 86%", recoveredTitle); + } + + [Fact] + public void UsageItemExposesIsolatedNotificationsThroughWinRtInterface() + { + var map = typeof(UsageDockItem).GetInterfaceMap(typeof(INotifyPropChanged)); + Assert.All(map.TargetMethods, method => Assert.Equal(typeof(UsageDockListItem), method.DeclaringType)); + } +} diff --git a/CodexUsageDock.Tests/UsageDataTests.cs b/CodexUsageDock.Tests/UsageDataTests.cs index 9ab9977..0ce998f 100644 --- a/CodexUsageDock.Tests/UsageDataTests.cs +++ b/CodexUsageDock.Tests/UsageDataTests.cs @@ -207,7 +207,7 @@ public void DetailsPageUsesTheProjectReleaseVersion() using var service = _environment.CreateService(); using var page = new CodexUsageDockPage(service, _environment.CreateSettings()); - Assert.Equal("0.8.5", CodexUsageDockMetadata.Version); + Assert.Equal("0.8.6", CodexUsageDockMetadata.Version); Assert.Equal($"Codex Usage - {CodexUsageDockMetadata.Version}", page.Title); } diff --git a/CodexUsageDock/CodexUsageDock.csproj b/CodexUsageDock/CodexUsageDock.csproj index ff30d51..5573cf9 100644 --- a/CodexUsageDock/CodexUsageDock.csproj +++ b/CodexUsageDock/CodexUsageDock.csproj @@ -3,7 +3,7 @@ WinExe CodexUsageDock app.manifest - 0.8.5 + 0.8.6 10.0.26100.68-preview net10.0-windows10.0.26100.0 diff --git a/CodexUsageDock/UsageDockListItem.cs b/CodexUsageDock/UsageDockListItem.cs index 9771c52..0b57069 100644 --- a/CodexUsageDock/UsageDockListItem.cs +++ b/CodexUsageDock/UsageDockListItem.cs @@ -1,16 +1,52 @@ +using System.Diagnostics; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; namespace CodexUsageDock; -internal partial class UsageDockListItem(ICommand command) : ListItem(command) +internal partial class UsageDockListItem : ListItem, INotifyPropChanged { + internal UsageDockListItem(ICommand command) : base(command) + { + // The pinned SDK stops multicast delivery after a failed COM subscriber. + // Bridge its notifications so an old host cannot block the current one. + base.PropChanged += ForwardPropertyChanged; + } + + public new event TypedEventHandler? PropChanged; + + private void ForwardPropertyChanged(object sender, IPropChangedEventArgs args) + { + var handlers = PropChanged; + if (handlers is null) return; + + foreach (TypedEventHandler handler in handlers.GetInvocationList()) + { + try + { + handler(this, args); + } + catch (Exception error) + { + // Only permanent RPC failures remove a subscriber. A busy host + // must still receive the next refresh after a transient error. + if (error.HResult is unchecked((int)0x80010108) or unchecked((int)0x800706BA)) + { + PropChanged -= handler; + } + + Trace.TraceWarning("Codex Usage Dock: display notification failed (HRESULT 0x{0:X8}).", error.HResult); + } + } + } + internal void NotifyDisplayPropertiesChanged() { // A host can subscribe after reading the initial values and miss an // intervening update. Refresh even unchanged values on the next read. - OnPropertyChanged(nameof(Title)); - OnPropertyChanged(nameof(Subtitle)); - OnPropertyChanged(nameof(Icon)); + ForwardPropertyChanged(this, new PropChangedEventArgs(nameof(Title))); + ForwardPropertyChanged(this, new PropChangedEventArgs(nameof(Subtitle))); + ForwardPropertyChanged(this, new PropChangedEventArgs(nameof(Icon))); } } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b428585..f98055e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -58,6 +58,10 @@ Always specify both `Platform` and its matching RID. Omitting `-r win-x64` or `- Tests must use `TestEnvironment` or explicitly inject both history stores and a temporary settings path. Only the production parameterless service constructor selects the current user's storage. Never instantiate it in a unit test. Each test owns and deletes its unique temporary directory; synthetic quota events must never reach real user history. Token tests await `TokenRefreshTask` separately because `RefreshAsync` completes when limits are published. Forecast arithmetic fixtures should use continuous measurements; gap and stale-data scenarios are tested separately. +### Dock notifications + +`DockNotificationTests` covers a remaining-allowance change from 93% to 86% with a failing host subscriber, permanent disconnection cleanup, transient recovery, and subscription through the WinRT interface. The pinned SDK stops multicast delivery after the first failing subscriber; `UsageDockListItem` bridges those notifications and isolates each recipient, matching the [upstream SDK fix](https://github.com/microsoft/PowerToys/blob/main/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/EventHelpers.cs). Preserve stable item and band identities and avoid provider catalog invalidation on routine refreshes. These tests verify notification delivery; use the integration matrix to verify the installed Dock rendering. + ### Chart labels The chart uses baked Segoe UI outlines because the Windows SVG image renderer does not support SVG text. `CodexUsageDock/ChartLabelGlyphs.cs` is generated source; regenerate it on Windows with `scripts/generate-chart-labels.ps1` and review the visual result. Runtime builds use the checked-in outlines and need no font loading or drawing dependency. @@ -158,6 +162,7 @@ Complete every row on a clean x64 environment and a separate clean ARM64 environ | Dock band | Required | Required | The band can be added, each enabled item opens details, and values update while Command Palette remains responsive. | | Settings | Required | Required | Visibility, reset-time, refresh-interval, and adaptive-forecast choices apply immediately and persist after restarting Command Palette. Verify that disabling pauses learning without replaying measurements collected while paused, retains history, and that deleting learned history requires confirmation. | | Live app-server | Required | Required | With a signed-in standalone Codex CLI, the details page identifies the CLI app-server as the source and refreshes live data. | +| Dock refresh | Required | Required | Pin the weekly Dock item and compare its percentage with the dashboard after manual and automatic refreshes that change the allowance. Repeat after **Reload Command Palette Extension** and leave the Dock running through several refreshes. The percentage keeps matching the dashboard without removing or repinning the band. | | Local fallback | Required | Required | In an isolated test account with local session metadata but no launchable standalone CLI, fallback data appears and is identified as local session data. Do not rename or delete a real CLI installation to create this state. | | No-data failure | Required | Required | In an isolated account with neither a CLI nor session data, the extension shows a bounded unavailable/error state and does not crash or loop. | From 4d22d11582770e2da6b696679724bfeecf4a6b02 Mon Sep 17 00:00:00 2001 From: Mathijs Beemsterboer <15211332+TheBeems@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:38:25 +0200 Subject: [PATCH 2/2] Link 0.8.6 changelog to release PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b062b89..ff4a40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ the [changelog workflow](AGENTS.md#documentation-and-change-quality). ### Fixed -- Keep Dock percentages updating when an old Command Palette notification subscriber disconnects. Deliver display changes independently to each subscriber, remove permanently disconnected subscribers, and retain transient failures for the next refresh. +- Keep Dock percentages updating when an old Command Palette notification subscriber disconnects. Deliver display changes independently to each subscriber, remove permanently disconnected subscribers, and retain transient failures for the next refresh. ([PR #33](https://github.com/TheBeems/CodexUsageDock/pull/33)) ### Changed