Repository navigation
Fix Windows notification color inversion - #211
Conversation
492bae9 to
2520cbd
Compare
rtibblesbot
left a comment
There was a problem hiding this comment.
Clean fix for the Windows notification icon color inversion (#184). Replaces legacy ShowBalloon() with wx.adv.NotificationMessage, which handles icon rendering correctly across Windows themes. CI passing.
- praise: Good defensive fallback chain and error handling throughout
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Reviewed the pull request diff checking for:
- Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
- Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
- Architecture: duplicated concerns, minimal interfaces, composition over inheritance
- Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
- Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
- Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
- Checked CI status and linked issue acceptance criteria
- For UI changes: inspected screenshots for layout, visual completeness, and consistency
| logging.error( | ||
| f"Failed to show notification with NotificationMessage: {notification_error}" | ||
| ) | ||
| self._show_legacy_notification(title, message, timeout) |
There was a problem hiding this comment.
✅ Resolved — addressed in the current code.
praise: Well-structured fallback chain: NotificationMessage → ShowBalloon → MessageBox. Each layer catches specific exceptions and logs before falling through, so notifications degrade gracefully on older Windows versions or unexpected runtime errors.
| # Cached icon used for notifications | ||
| self._tray_icon = None | ||
| # Keep reference alive while shown | ||
| self._last_notification = None |
There was a problem hiding this comment.
✅ Resolved — addressed in the current code.
praise: Good call keeping _last_notification as an instance attribute — prevents the NotificationMessage object from being garbage-collected while the notification is still visible.
2520cbd to
8519ef3
Compare
|
Hi @rtibbles - unfortunately the app is not starting in both Windows 11 and 10, seems that this is the relevant error in the logs: Logs: logs.zip |
8519ef3 to
4303712
Compare
🔵 Review postedLast updated: 2026-07-17 01:04 UTC |
rtibblesbot
left a comment
There was a problem hiding this comment.
3 of 3 prior findings resolved — the prior approach (NotificationMessage) was abandoned entirely in favor of a binary icon re-encode, so the Timeout_Default crash and both praise comments on that removed code are moot.
Still open (not caused by this PR, but blocking a green merge):
- important: CI's "All file linting" (
uv-lockprek hook) is failing, but this predates this branch — confirmed the same job fails onmain's tip (a9f1e13, this branch's merge-base) via the check-runs API.pyproject.toml'sexclude-newer = "7 days"is a rolling window, souv.lockneeds periodic re-resolution independent of this diff. Nothing in this PR (kolibri.icoonly) can fix it — needs auv lockregen onmainand a rebase. - suggestion: Since this PR no longer touches
taskbar_icon.py, @pcenov's crash report was against the abandoned approach. Worth a short comment confirming they've re-tested against this narrower icon-only fix.
Verified the re-encoded kolibri.ico directly: 16/24/32/48px are now raw BMP (matching the Shell_NotifyIcon legacy balloon path that caused the original inversion), 64/128/256px remain PNG. Decoded all 7 sizes — consistent yellow/blue colors, no inversion.
Prior-finding status
RESOLVED — src/kolibri_app/taskbar_icon.py — NotificationMessage.Timeout_Default crash reported by pcenov (approach replaced with icon re-encode, not patched)
RESOLVED — src/kolibri_app/taskbar_icon.py:115 — praise: defensive NotificationMessage → ShowBalloon → MessageBox fallback chain (code removed, no longer applicable)
RESOLVED — src/kolibri_app/taskbar_icon.py — praise: _last_notification instance attribute (code removed, no longer applicable)
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Compared the current PR state against findings from a prior review:
- Retrieved prior bot reviews via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Ran the same phased review passes as a first review (core, frontend/backend lenses, manual QA when required)
- Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
4303712 to
e0ea2a1
Compare
rtibblesbot
left a comment
There was a problem hiding this comment.
PR #211 — All prior code/notification findings are resolved (the NotificationMessage/balloon approach was abandoned; the fix is now a pure kolibri.ico re-encode). The re-encode is functionally correct — I decoded every size and confirmed the true colors (yellow #FFCB00 background, blue #3862FA bird) with no inversion across the legacy XOR-mask sizes. One open suggestion and a pre-existing CI failure remain.
suggestion — src/kolibri_app/icons/kolibri.ico: All 7 sizes are now uncompressed 32-bpp BMP, contradicting the PR description ("re-encode 16/24/32/48 as BMP; keep 64/128/256 as PNG"). Only the small sizes (16–48px) exercise the legacy balloon path that caused the inversion — 64/128/256 render via the modern PNG path. Restoring PNG for the three large sizes would shrink the artifact from ~372 KB back to ~140 KB with no functional loss. Either do that, or update the description to reflect the all-BMP encoding and the ~7× size increase over the original 52 KB. Not blocking.
CI — not fixable here: The red "All file linting" check is the uv-lock prek hook, which also fails on the merge-base (a9f1e13, current main tip). It predates and is independent of this icon-only change (rolling exclude-newer window needs a periodic uv.lock regen + rebase on main). Flagging so the red isn't mistaken for a fault in this diff.
Prior-finding status
RESOLVED — src/kolibri_app/taskbar_icon.py — NotificationMessage.Timeout_Default crash (approach abandoned; file no longer touched)
RESOLVED — src/kolibri_app/taskbar_icon.py:115 — praise: NotificationMessage → ShowBalloon → MessageBox fallback chain (code removed)
RESOLVED — src/kolibri_app/taskbar_icon.py — praise: _last_notification instance attribute (code removed)
ACKNOWLEDGED — CI uv-lock linting failure (pre-existing on main, cannot be fixed from this icon-only PR)
UNADDRESSED — process: confirm @pcenov re-tested against the narrower icon-only fix (no code impact)
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:
- Retrieved prior bot reviews via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
- Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
e0ea2a1 to
000c039
Compare
rtibblesbot
left a comment
There was a problem hiding this comment.
PR #211 — delta re-review of the new Shell_NotifyIcon/NOTIFYICONDATAW ctypes path. Struct layout, cleanup, and wndproc subclassing look correct; no blocking issues. All prior findings resolved (the previous wx.adv.TaskBarIcon/NotificationMessage approach was abandoned and rewritten).
Two hardening suggestions inline. CI: only the All file linting (uv-lock prek) hook fails — same rolling exclude-newer window that is red on main's tip, not fixable from this diff. Note: the new hand-rolled tray path is untested on-device — a manual right-click / notification pass on Win10/11 is worth doing before merge.
Prior-finding status
RESOLVED — src/kolibri_app/taskbar_icon.py — NotificationMessage.Timeout_Default crash (file rewritten to Shell_NotifyIcon path)
RESOLVED — src/kolibri_app/taskbar_icon.py:115 — praise: NotificationMessage → ShowBalloon → MessageBox fallback (code removed)
RESOLVED — src/kolibri_app/taskbar_icon.py — praise: _last_notification instance attribute (code removed)
RESOLVED — src/kolibri_app/icons/kolibri.ico — all-BMP re-encode (icon approach abandoned; PR now code-only)
ACKNOWLEDGED — CI uv-lock linting failure (still red on 000c039; same as main tip, not fixable from this diff)
UNADDRESSED — process: on-device retest (crashing approach gone, but new Shell_NotifyIcon path itself untested — see menu-QA note)
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:
- Retrieved prior bot reviews via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
- Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
| except (ImportError, AttributeError, OSError) as e: | ||
| nid = _new_nid(self.hwnd) | ||
| nid.uFlags = NIF_INFO | ||
| nid.szInfo = message |
There was a problem hiding this comment.
✅ Resolved — addressed in the current code.
suggestion: nid.szInfo is WCHAR * 256 and nid.szInfoTitle is WCHAR * 64; assigning a longer Python string raises ValueError: string too long, which isn't in the caught (OSError, pywintypes.error) tuple below — so it escapes the MessageBox fallback and propagates on the startup path. notify_server_failed builds …Check logs at: {log_path} from KOLIBRI_HOME, and a deep profile path can push past 255 chars. Cheap to harden: truncate (message[:255], title[:63]) or add ValueError to the except clause.
| self._remove_icon() | ||
| return win32gui.CallWindowProc(self._old_wndproc, hwnd, msg, wparam, lparam) | ||
|
|
||
| def _show_menu(self): |
There was a problem hiding this comment.
✅ Resolved — addressed in the current code.
suggestion: The right-click menu is now hand-rolled — SetForegroundWindow(self.hwnd) on the hidden tray frame then frame.PopupMenu(menu), where wx.adv.TaskBarIcon previously managed the popup lifecycle. Worth a manual Win10/11 check that the menu dismisses on click-away and all four items fire; I can't verify this without a Windows session.
000c039 to
679801c
Compare
rtibblesbot
left a comment
There was a problem hiding this comment.
PR #211 — delta re-review. All prior findings resolved or acknowledged; no new issues in the newly changed code. The szInfo/szInfoTitle over-long-string risk is fixed via message[:255]/title[:63] truncation. The hand-rolled struct layout, wndproc subclassing, icon lifecycle, and symmetric teardown all check out.
CI is red only on the pre-existing uv-lock hook (already failing on main tip, not fixable from this diff), so this stays a COMMENT rather than an approval. One item remains for the manual-QA pass, not a code defect: verify on Win10/11 that the right-click menu dismisses on click-away, all four items fire, and the startup/ready balloons render with non-inverted colours before merge.
Prior-finding status
RESOLVED — src/kolibri_app/windows/taskbar_icon.py:255 — szInfo/szInfoTitle over-long-string ValueError (now truncated to 255/63)
ACKNOWLEDGED — src/kolibri_app/windows/taskbar_icon.py:226 — hand-rolled right-click menu + balloon path need on-device manual QA (deferred, not a code defect)
ACKNOWLEDGED — CI uv-lock linting failure (pre-existing on main tip, not fixable from this diff)
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:
- Retrieved prior bot reviews via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
- Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
wxPython's ShowBalloon drops wxWidgets' balloon-icon parameter, so Windows XOR-draws the tray HICON for the notification: colors invert in light theme and the icon renders low-res. Re-encoding kolibri.ico can't help — the balloon reuses the already-decoded in-memory tray HICON, never the file. Drive the tray icon directly through Shell_NotifyIcon on a hidden wx.Frame and send the balloon with NIIF_USER | NIIF_LARGE_ICON and a 128px alpha hBalloonIcon, which the shell alpha-blends at full resolution. ctypes is required because pywin32's Shell_NotifyIcon tuple stops at hIcon and cannot carry hBalloonIcon. Move the module into a new kolibri_app.windows subpackage. Fixes #184 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKRPVQmipak5UCTkP3sgXm
Toast body icon (hBalloonIcon) already renders correctly; the remaining defect is the toast header, which the shell attributes to an auto-generated identity: title "KolibriApp.exe" and an icon extracted from the tray HICON that renders with swapped colours (#184). Registering an AUMID with DisplayName and IconUri and adopting it via SetCurrentProcessExplicitAppUserModelID before the tray icon exists makes the shell title toasts "Kolibri" and render the header icon straight from the .ico file. Verified on Windows 11 26200 (light theme) by replicating both API sequences in-VM: without AUMID the header icon colour-swaps; with it, header and body render correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KpzCncQjENrZafLJVS4EfE
679801c to
a2c6ff9
Compare
rtibblesbot
left a comment
There was a problem hiding this comment.
PR #211 — delta re-review of the two new commits (c10de40 rebased the Shell_NotifyIcon/NOTIFYICONDATAW path; a2c6ff9 registers an explicit AppUserModelID and adopts it). AUMID additions are small, well-guarded, and read correctly. No blocking issues. All 4 prior findings resolved/acknowledged.
Two low-severity notes on the new AUMID code (inline). CI passing.
Prior-finding status
RESOLVED — src/kolibri_app/windows/taskbar_icon.py:255 — szInfo/szInfoTitle over-long-string truncation
RESOLVED — src/kolibri_app/taskbar_icon.py — NotificationMessage fallback chain / _last_notification retention (praise)
RESOLVED — src/kolibri_app/taskbar_icon.py — NotificationMessage.Timeout_Default crash (approach abandoned)
ACKNOWLEDGED — src/kolibri_app/windows/taskbar_icon.py:260 — hand-rolled right-click menu + balloon/AUMID header path need on-device manual QA
ACKNOWLEDGED — CI uv-lock linting (pre-existing on main tip, not fixable from this diff)
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:
- Retrieved prior bot reviews via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
- Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
| winreg.HKEY_CURRENT_USER, REG_KEY_APP_USER_MODEL_ID | ||
| ) as key: | ||
| winreg.SetValueEx(key, "DisplayName", 0, winreg.REG_SZ, APP_NAME) | ||
| winreg.SetValueEx(key, "IconUri", 0, winreg.REG_SZ, icon_path) |
There was a problem hiding this comment.
suggestion: Whether a classic Shell_NotifyIcon NIF_INFO balloon honors this AppUserModelId\...\IconUri value for its toast header icon depends on shell version — I can't confirm it from code. The balloon body icon is already handled directly via NIIF_USER | hBalloonIcon, so this registry write is harmless if it has no effect. Worth verifying in the on-device QA pass that the header shows the correct, non-inverted icon.
There was a problem hiding this comment.
Verified on-device (Win11 26200, light theme): with the registered AUMID the balloon toast header shows "Kolibri" and renders the IconUri icon with correct colours. Without it, the header showed "KolibriApp.exe" with a colour-swapped icon extracted from the tray HICON.
There was a problem hiding this comment.
Thanks for verifying — that confirms the registry write is doing real work (correct header name and non-inverted icon). Good to know the difference is that clear-cut.
| self._add_icon() | ||
|
|
||
| self.set_icon(TRAY_ICON_ICO, f"{APP_NAME}") | ||
| def _resolve_icon_path(self): |
There was a problem hiding this comment.
nitpick: _resolve_icon_path reimplements files("kolibri_app") / TRAY_ICON_ICO (already in view.py:72 and about_dialog.py:29). The .resolve() is justified here since AUMID IconUri/SetCurrentProcessExplicitAppUserModelID want an absolute path — a shared "absolute path to packaged resource" helper would keep the three call sites in sync. Take-it-or-leave-it.
There was a problem hiding this comment.
Leaving as-is: the other two call sites don't need absolute paths, so a shared helper would only sync an incidental detail. Happy to revisit if a third absolute-path consumer appears.
There was a problem hiding this comment.
Makes sense — the absolute path is genuinely incidental to the other two sites, so a shared helper would couple them without cause. Agreed on revisiting only if a third absolute-path consumer shows up.
|
Hi @rtibbles, I confirm that the color inversion is fixed. |
|
Merging based on manual QA confirmation. |
Summary
Fixes #184 — Kolibri icon rendered with swapped colours (yellow↔blue) in Windows notifications on light theme.
Two defects, two fixes:
wx.adv.TaskBarIcon.ShowBallooncannot pass a balloon icon, so the shell rasterizes the trayHICONthrough a legacy path that mangles colours. Replaced with a directShell_NotifyIconimplementation (ctypesNOTIFYICONDATAW) sendingNIIF_USER | NIIF_LARGE_ICONwith a 128pxhBalloonIcon.HICON. Now registersHKCU\Software\Classes\AppUserModelIdwithDisplayName/IconUriand adopts it viaSetCurrentProcessExplicitAppUserModelID.The
.icoencoding is not a factor: PNG- and BMP-encoded entries render identically through both paths.Testing
On-device with this branch's CI build installed:
Screenshots
Before (prior commit):
After (this build):
🤖 Generated with Claude Code
https://claude.ai/code/session_01KpzCncQjENrZafLJVS4EfE