Windows tray: menu parity with macOS, movable panel, UTF-8 hook output#75
Conversation
Adds the five macOS menu features the Windows tray was missing: AI daily-report link, hide-sections submenu (Claude/Codex/Antigravity), quota threshold notifications via pystray toasts, session-resume and terse-mode toggles backed by the shared session_hooks module. talent_market stays excluded (its vendored CLI is macOS-only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Injects a shared drag handle (pywebview-drag-region) into every panel theme via the JS shim, small enough not to overlap the quota-card drag-to-reorder areas. The window position is saved to prefs on hide, restored (clamped to the work area) on the next open, and a new tray menu item resets it to the default tray-adjacent placement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The in-panel "Switch Panel" button used to cycle to the next theme; it now opens a localized HTML overlay menu mirroring the macOS popup: theme submenu with the active panel checked, hide-sections toggles, AI daily report, refresh, launch-at-login, quota notifications, session-resume, terse mode, reset panel position, update check, and quit. Menu state is re-queried from Python on every open, and theme switches keep the deferred-reload guard that prevents blank windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The statusLine hook printed through a cp950-encoded pipe on Windows, so Claude Code (which decodes UTF-8) rendered the progress-bar glyphs as U+FFFD; and --setup crashed with UnicodeEncodeError printing its localized check-mark message to a cp950 console. All hook scripts and the setup/session-hooks CLI paths now reconfigure stdout/stderr to UTF-8 on Windows (guarded, stdlib-only, Python 3.9-safe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tests reached the sys module through the module under test (usage_statusline.sys), which mypy strict rejects as an implicit re-export; patching the imported sys module directly is the same object and behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ture Reorders the panel menu to mirror the macOS grouping with the Windows-only utilities at the bottom, expands the theme/hide-sections submenus in place (indented accordion inside the scrollable menu) so long lists are never clipped by the window, and neutralizes the quota card drag-to-reorder gesture on Windows via a capture-phase listener - it was too easy to confuse with the window drag handle. Saved card order still renders, and macOS keeps its drag behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pressing an empty area of the Claude/Codex/Antigravity cards now moves the whole window (a temporary pywebview-drag-region on the card, so buttons and links inside keep their normal click path), with grab and grabbing cursors on both the cards and the top handle. The in-panel menu drops Check for Updates, Reset Panel Position, and Quit - all still reachable from the tray menu or the panel footer - ending at Refresh Now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new switch-panel menu test built the full menu (including the login-item toggle) without mocking win_login_item.is_enabled, which imports winreg — crashing CI's macOS check job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e57c24f397
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| def _configure_windows_utf8_output() -> None: | ||
| """Make hook output UTF-8 when Claude Code reads a Windows pipe.""" | ||
| if os.name != "nt": | ||
| return | ||
| for stream in (sys.stdout, sys.stderr): | ||
| with suppress(AttributeError, OSError, ValueError): | ||
| # Test runners and embedders may replace the TextIOWrapper streams. | ||
| cast(Any, stream).reconfigure(encoding="utf-8") |
There was a problem hiding this comment.
This UTF-8 setup is added to scripts that are copied into ~/.claude, but their installed-version gates remain unchanged: setup_hook.needs_update() only refreshes usage-statusline.py when the installed __version__ differs from HOOK_VERSION, and the resume hook uses the same RESUME_HOOK_VERSION pattern. For users upgrading with existing 1.0/1.6 hooks, startup self-heal treats the old copies as current, so the Windows cp950 fix never reaches them. Please bump the hook version constants whenever adding behavior to copied hook scripts.
Useful? React with 👍 / 👎.
| def main() -> None: | ||
| _configure_windows_utf8_output() |
There was a problem hiding this comment.
Decode forwarded hook output as UTF-8
In Windows coexistence mode, this only reconfigures the forwarder's own stdout; _run_hook still uses subprocess.run(..., text=True, capture_output=True) without an encoding, so Python decodes the child hook's now-UTF-8 stdout with the process locale such as cp950. When the status line contains block/emoji/check glyphs, that raises UnicodeDecodeError and is caught as an empty string, dropping the usage line for users with another statusLine tool. Capture bytes or decode captured stdout as UTF-8 instead of relying on the locale.
Useful? React with 👍 / 👎.
Summary
--setupcrashed with UnicodeEncodeError printing ✓ on a cp950 consoleTesting
ruff check --exclude release-win ./mypy . --exclude release-wincleanpytest -q: 737 passed, 2 skipped (POSIX-only) on Windows--setupexit 0 on cp950🤖 Generated with Claude Code