Skip to content

build(monorepo): fold openadapt-tray into packages/tray (with history) - #1356

Open
abrichr wants to merge 89 commits into
mainfrom
monorepo/tray
Open

abrichr wants to merge 89 commits into
mainfrom
monorepo/tray

Conversation

@abrichr

@abrichr abrichr commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Hold for the Desktop release. This PR touches packages/desktop/ (tests/test_native_release.py and docs/COMPLIANCE.md) and .github/workflows/desktop-test.yml. Merge it after Desktop 0.21.1 (#1382) has published.

Merge only with "Create a merge commit". Squash or rebase would drop the imported history.

Folds OpenAdaptAI/openadapt-tray into packages/tray with its history, the same way as privacy (#1266) and desktop (#1267).

Import

  • Freeze: openadapt-tray main @ e979dbbf77439240daac7df0d33d708e7d0cf2c6 (82 commits, v0.3.7). Fetched into a scratch repository made with git init and git fetch (no clone, no tags).
  • Rewritten with git filter-repo --to-subdirectory-filter packages/tray --preserve-commit-hashes, then joined by one --allow-unrelated-histories --no-ff merge (abcd416). No existing history is rewritten.
  • Scans over every imported commit: gitleaks with the default rules and with OpenAdapt's .gitleaks.toml found nothing. The private-name guard over messages, paths and added lines is clean.
  • docs/monorepo/openadapt-tray-commit-map.tsv maps old hashes to new ones. The fold table in docs/monorepo/README.md lists tray.

Release

  • The PyPI name stays openadapt-tray. Releases change from semantic-release on every push to the manual Release package workflow, through a new pypi-tray environment, with the tag openadapt-tray-v<version>. scripts/verify_release_hosting.py accepts that tag.
  • The package drops its .github directory, [tool.semantic_release], the locked release-tool project (scripts/release/) and its wrapper scripts. tests/test_release_contract.py keeps the version and lock checks and adds two: the changelog starts at the project version, and the release workflow lists tray.
  • The changelog stopped at v0.2.0 because semantic-release wrote later notes only to GitHub Releases. It's backfilled from the v0.2.1 to v0.3.7 releases. The commit links still resolve in OpenAdaptAI/openadapt-tray, because that history was never rewritten.
  • Nothing is published by this PR.

CI (path-filtered)

  • packages.yml gets a tray scope (^packages/tray/ or the workflow itself), so tray jobs run only when the tray changes:
    • tray: tests on Linux with Python 3.10 and 3.12 for a PR. On main, it also runs 3.11, macOS and Windows. That's 2 jobs per PR, where the old repository ran 5.
    • tray-contract: ruff over the whole package, uv build, version agreement, and the archive boundary.
    • tray-desktop-ipc: Desktop's tests/test_tray_integration.py against this tray client, both from the same commit.
    • All three are in packages-gate.
  • desktop-test.yml no longer checks out openadapt-tray at a pinned commit. It points OPENADAPT_TRAY_SOURCE at ../tray/src, so a Desktop change tests against the tray in the same tree.
  • Dependabot watches packages/tray/uv.lock.

Docs

README package table, CAPABILITIES.md (the package list and a tray row under "Recording, privacy and proof"), CONTRIBUTING.md, CLAUDE.md, docs/packages/index.md, docs/design/openadapt-tray.md (a status note: the tray ships from packages/tray and how it differs from this design) and packages/desktop/docs/COMPLIANCE.md. llms-full.txt is regenerated.

Checked locally

  • packages/tray: uv sync --locked --extra dev, ruff check . and pytest tests (335 passed, 2 skipped); uv build, check_release_consistency.py --require-dist and check_source_boundary.py --require-dist pass.
  • packages/desktop: OPENADAPT_TRAY_SOURCE=../tray/src pytest tests/test_tray_integration.py passed.
  • Root: tests/test_release_hosting.py (19 passed), scripts/check_source_boundary.py OK, actionlint clean. One root test, test_cli_dead_ends.py::test_other_unknown_commands_keep_clicks_suggestion, fails in my local environment because Click doesn't print "Did you mean". This PR doesn't touch the CLI.

Not done here

  • Release setup outside the repository, for the founder: create the pypi-tray environment (deployment branch main), add the refs/tags/openadapt-tray-v* tag rulesets (copy the openadapt-flow ones), and add a PyPI trusted publisher for openadapt-tray (owner OpenAdaptAI, repository OpenAdapt, workflow release-package.yml, environment pypi-tray).
  • The install-weight job doesn't weigh tray yet. import openadapt_tray needs a display on a headless Linux runner, and weigh.py can't set PYSTRAY_BACKEND=dummy.
  • openadapt-tray isn't archived. It gets a pointer README in a separate PR there; its release.yml stays until the first monorepo release, and archiving is the founder's call.

🤖 Generated with Claude Code

abrichr and others added 30 commits January 17, 2026 00:23
This commit implements the openadapt-tray system tray application for OpenAdapt,
based on the design document at docs/design/openadapt-tray.md.

Features:
- Cross-platform system tray icon using pystray (lightweight alternative to Qt)
- Start/Stop recording menu items that delegate to openadapt CLI
- Global hotkeys (Ctrl+Shift+R for recording, Ctrl+Shift+D for dashboard)
- Triple-ctrl stop sequence for legacy compatibility
- State machine for application state management
- Platform-specific handlers for macOS, Windows, and Linux
- Native dialogs using AppleScript (macOS), ctypes (Windows), and zenity/kdialog (Linux)
- Notification support across all platforms
- Configuration persistence in platform-appropriate locations
- IPC client for communication with other OpenAdapt processes
- Placeholder icons (colored circles) for different states

Package structure:
- src/openadapt_tray/ - Main package code
  - app.py - Main TrayApplication class
  - menu.py - Menu construction
  - icons.py - Icon management
  - notifications.py - Cross-platform notifications
  - shortcuts.py - Global hotkey handling
  - config.py - Configuration management
  - ipc.py - Inter-process communication
  - state.py - Application state machine
  - platform/ - Platform-specific handlers
- assets/icons/ - State icons (idle, recording, training, error)
- tests/ - Test suite (78 passing tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add standard badges for license and Python version. PyPI badges are
commented out until the package is published.

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add README badges for license and Python version

Add standard badges for license and Python version. PyPI badges are
commented out until the package is published.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: implement desktop-notifier notification system

Implement modern, native notification system using desktop-notifier library
based on comprehensive review in task a95f1e8. Replaces legacy AppleScript/
PowerShell implementations with cross-platform solution.

Key features:
- Native notifications on macOS (Notification Center), Windows (WinRT),
  and Linux (DBus)
- Click callbacks for notification interactions
- Urgency levels (low, normal, critical)
- Action buttons (platform-dependent)
- Reply fields (macOS only)
- Custom icons and duration control
- Async and sync APIs
- Graceful fallback to platform-specific implementations
- Full backward compatibility with existing API

Changes:
- Completely rewrote src/openadapt_tray/notifications.py (390 lines)
  - Added desktop-notifier backend with event loop management
  - Maintained fallback to AppleScript/PowerShell/notify-send
  - Implemented sync wrapper around async desktop-notifier API
  - Added resource cleanup on app shutdown

- Updated src/openadapt_tray/app.py
  - Added notifications.cleanup() call in quit() method

- Updated pyproject.toml
  - Added desktop-notifier>=6.2.0 dependency

- Updated README.md
  - Added native notifications to features list
  - Added desktop-notifier to dependencies

Documentation:
- Added NOTIFICATIONS.md: Comprehensive notification system guide (9.4 KB)
  - Usage examples for all features
  - Architecture and backend detection
  - Platform-specific notes and troubleshooting
  - Complete API reference
  - Migration guide

- Added examples/notification_examples.py: 10 comprehensive examples
  - Basic notifications
  - Callbacks and urgency levels
  - Action buttons and reply fields
  - Conditional and state-based notifications

- Added test scripts:
  - test_notification_simple.py: Quick non-interactive test
  - test_notification.py: Interactive test with user interaction
  - examples/README.md: Example documentation

- Added IMPLEMENTATION_SUMMARY.md: Complete implementation documentation

Testing:
- Verified backend detection and fallback mechanisms
- Tested basic, critical, and callback notifications
- Confirmed backward compatibility with existing API
- Manual testing available via test_notification_simple.py

Benefits:
- Modern native notification experience
- Better platform integration
- Rich interactive features
- Clean, maintainable API
- No breaking changes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: detect app bundle on macOS and fall back to AppleScript

Fixes critical bug where desktop-notifier fails with "'NoneType'
object is not iterable" when not running from an app bundle on macOS.

Changes:
- Add app bundle detection in _detect_backend() method
- Check both APP_BUNDLE env var and bundle path structure
- Automatically fall back to AppleScript if not in app bundle
- Add os import for environment variable checking
- Fix README.md desktop-notifier version (6.0.0 -> 6.2.0)

The AppleScript fallback works perfectly for non-bundled execution
(e.g., development, CLI usage) while desktop-notifier can still be
used when running from a proper macOS app bundle.

Tested: All notifications now work correctly via AppleScript fallback.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: Improve notification backend detection and make IPC optional

Fixes two critical startup issues:

1. Notification Center app bundle requirement:
   - Try to initialize desktop-notifier even outside app bundle
   - Gracefully fall back to AppleScript if initialization fails
   - Provides better error messages for debugging

2. IPC connection blocking startup:
   - Wrap IPC connection in try-except to prevent startup failures
   - Add informative messages for connection status
   - App now runs in standalone mode if IPC server unavailable

These changes make the tray app more resilient and provide better
user experience when running outside of a full app bundle or when
the OpenAdapt backend is not running.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: Add automated screenshot capture workflow

Simplifies screenshot capture and organization workflow with two new files:

1. rename_screenshots.py:
   - Automatically finds recent screenshots from Desktop
   - Interactive renaming with confirmation
   - Moves to docs/screenshots/ with standardized names
   - Backs up existing files to prevent data loss
   - Supports both "Screen Shot" and "Screenshot" formats (macOS variations)
   - Shows timestamps to help verify correct screenshot order

2. SCREENSHOT_SIMPLE.md:
   - Step-by-step guide for simplified screenshot workflow
   - Takes 10-15 minutes total
   - Just take screenshots with Cmd+Shift+4, run script, commit
   - No manual renaming needed
   - Includes troubleshooting section
   - Covers required and optional screenshots

User workflow:
  1. Take 5-8 screenshots with Cmd+Shift+4
  2. Run: python rename_screenshots.py
  3. Commit: git add docs/screenshots && git commit

This makes documentation screenshots much easier to capture and
organize, reducing manual work from 30+ minutes to 10-15 minutes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- publish.yml: Triggered on tags, publishes to PyPI
- release.yml: Auto-bumps version on PR merge, creates tags

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replaces manual commit parsing with python-semantic-release:
- Automatic version bumping based on conventional commits
- feat: -> minor, fix:/perf: -> patch
- Creates GitHub releases automatically
- Publishes to PyPI on release

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The python-semantic-release action runs in a Docker container where uv
is not available. Let the workflow handle building instead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace `branch = "main"` (v7/v8 key) with `[tool.semantic_release.branches.main]`
table (v9 key). The old key is silently ignored by v9, causing releases to never
trigger on the main branch.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reposition the Experimental tray around record, compile, replay, and governed hosted repair, with protected-main and pre-1.0 release safeguards.
Replace the undefined release lock variable with an offline exact-root synchronizer and preserve v0.1.0 as the failed-attempt audit record.
Supersedes the original whole-file rewrite: main's README (from #5)
already covers the loop status/launcher role, so this PR now only fixes
the claims that went stale when the hosted lifecycle merged and 0.1.1
shipped to PyPI:

- hosted behavior is on main and released, not 'feat/hosted-rewire branch code'
- version is 0.1.1 (pre-alpha classifier), not 0.0.1
- add the PyPI badge and a real pip install quickstart
- keep the honest boundary: no released openadapt-desktop build provides
  the companion IPC service yet

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Match the AGENTS.md / org-profile / LIMITS.md canonical one-liner
('retained evidence'); the tray README said 'recorded evidence'.


Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ot) (#9)

Adds CodeQL SAST, gitleaks secret scanning, dependency review, Dependabot, and a vulnerability-disclosure policy as part of SOC 2 readiness. Additive and non-breaking.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replace the plain solid colored circle with the OpenAdapt brand mark
(the chat/robot face from the openadapt.ai favicon), color-stylized per
recording-lifecycle state so the state stays readable at a glance while
the brand is finally shown in the menu bar / system tray.

- Add the vector mark (assets/mark.svg) and a high-res transparent raster
  master (assets/mark-master.png), plus a 512px copy packaged inside the
  wheel (src/openadapt_tray/assets/mark-master.png) so the installed app
  can render the mark. The wheel previously shipped no assets, so it
  always fell back to a generated circle.
- IconManager now tints the mark's alpha silhouette per state
  (idle=brand blue, recording=red, starting/stopping=amber,
  compiling=purple, error=red). One tint implementation is shared by the
  runtime and the generator, so committed PNGs and runtime icons never
  diverge. The needs-attention badge is preserved.
- Give every TrayState its own icon file. This fixes a latent bug where
  COMPILING referenced a nonexistent compiling.png (fell back to a
  circle) and starting/stopping showed red instead of amber.
- Rewrite scripts/generate_icons.py to re-render the master from the SVG
  (rsvg-convert/cairosvg/inkscape) and tint per-state 1x + @2x PNGs and a
  multi-size logo.ico. Remove the stale training.png icons.

Icons verified valid at menu-bar sizes (22px + 44px, light and dark);
137 tests pass, ruff clean.


Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#10)

Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.9.0 to 5.0.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](actions/dependency-review-action@2031cfc...a1d282b)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [python-semantic-release/publish-action](https://github.com/python-semantic-release/publish-action) from 9.15.2 to 10.6.1.
- [Release notes](https://github.com/python-semantic-release/publish-action/releases)
- [Changelog](https://github.com/python-semantic-release/publish-action/blob/main/releaserc.toml)
- [Commits](python-semantic-release/publish-action@b9c41d4...5a5718c)

---
updated-dependencies:
- dependency-name: python-semantic-release/publish-action
  dependency-version: 10.6.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@ece7cb0...5fda3b9)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.15.2 to 10.6.1.
- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases)
- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.rst)
- [Commits](python-semantic-release/python-semantic-release@7b3f716...39dd205)

---
updated-dependencies:
- dependency-name: python-semantic-release/python-semantic-release
  dependency-version: 10.6.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ntract (#18)

Rewrite the README around the shared OpenAdapt positioning (governed
demonstration compiler; substrates first-class with an honest maturity ladder;
local-first; MIT open-core) and describe what the tray actually does: start/stop
recording, recent captures, needs-attention count and routing, open desktop app
or cloud dashboard, sync channel, login, and settings.

Document the per-state OpenAdapt-mark icon (one mark tinted per recording/compile
lifecycle state) and how the tray fits with openadapt-desktop and the cloud.

Update the stale cross-repo claim: the openadapt-desktop main branch now serves
the exact discovery-socket and command contract this tray expects, so the README
no longer says no build implements it. It still states plainly that the two
surfaces have not been validated together end to end and that no signed desktop
build ships the server yet. Update the metadata test to assert that honest
boundary instead of the retired string. Cross-link docs.openadapt.ai. No em dashes.


Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps-dev): bump uv from 0.11.26 to 0.11.29

Bumps [uv](https://github.com/astral-sh/uv) from 0.11.26 to 0.11.29.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.11.26...0.11.29)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.11.29
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps-dev): synchronize uv 0.11.29 lock

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: abrichr <richard.abrich@mldsai.com>
…er (#26)

Bumping only one of init/analyze leaves the pair at mismatched versions, which
CodeQL rejects with a configuration error. Bump both in one commit and group
future codeql-action updates so Dependabot cannot split them again.


Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.3.2 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@11f9893...c771a70)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.14.0 to 1.14.1.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](pypa/gh-action-pypi-publish@cef2210...ba38be9)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.14.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dependabot Bot and others added 26 commits September 10, 2026 20:34
Update the pinned publishing action to version 10.6.2.
Separate release dependencies from the application and validate the pinned PSR runtime.
Update the isolated release dependency, lock, wrapper, and contract to GitPython 3.1.61.
Use UV 0.12.7 in the isolated release environment, workflow, lock, and contract.
Align the build backend and locked release tools, and verify the distribution metadata.
Update the reviewed UV and GitPython tools and preserve PSR Click compatibility.
Update the pinned CodeQL action for Python analysis.
Update the pinned setup action while retaining the existing release tool version.
Keep the release bootstrap pin, isolated tool requirement, resolver lock, and release contract checks synchronized.
* fix: show the recording hotkey in the platform's form in the tray menu

The idle menu item read "Start Recording (<ctrl>+<shift>+r)". The label
used the hotkey setting as written, which is pynput's registration syntax,
not something a person reads.

Add format_hotkey() to shortcuts.py. It parses the setting the way pynput
does and shows it as "⌃⇧R" on macOS and "Ctrl+Shift+R" on Windows and
Linux, with the Command key shown as Win on Windows and Super on Linux.
A setting pynput would reject is shown unchanged, so a bad value still
appears in the menu instead of breaking it. The menu label uses the
formatted form; the stored setting and the hotkey registration keep the
raw string. The README menu sketch now shows the formatted default.

Ledger: B121

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the tray menu building when the hotkey setting is not a string

format_hotkey() iterated over the setting directly, so a tray.json with
"toggle_recording": null raised TypeError and the menu failed to build.
Before the label change the same file showed "Start Recording (None)".
A value that is not a string is now shown as str() gives it.

A setting with more than one character outside angle brackets
("ctrl+shift+r") or empty brackets was shown as "CTRLSHIFTR" on macOS,
dressed up as a shortcut pynput would never register. Such a setting is
now shown unchanged, which is what the docstring already promised. The
docstring now says key names are not checked against pynput's list.

Also rejoin a README paragraph that the label change left broken
mid-sentence.

Ledger: B121

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Replace source-policy.public.json with the render of the canonical policy
on openadapt-internal main (policy_last_updated 2026-09-03).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(macos): keep the hotkey listeners from aborting the tray at startup

On macOS 15, `openadapt-tray` 0.3.5 from PyPI exits with SIGABRT about a
second after it starts. pystray creates an NSApplication, and pynput's
listener threads then read the keyboard layout with the Text Input Sources
API off the main thread, which macOS aborts. A two-line repro (create the
NSApplication, start a GlobalHotKeys listener) aborts the same way.

HotkeyManager.start() now reads the layout once on the main thread and gives
pynput's listeners that value, which matches what pynput does per listener.
If the layout can't be read, or start() runs off the main thread, the tray
starts no listener and says the hotkeys are disabled instead of crashing.

Checked on macOS 15.7.3 with Python 3.12: the PyPI 0.3.5 wheel exits 134;
a wheel built from this branch keeps running until it is stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(macos): stop printing an import error on every start

The macOS backend probe imported `desktop_notifier.macos`, which
desktop-notifier 6 doesn't have, so every start printed "desktop-notifier
not available (No module named 'desktop_notifier.macos')" before falling
back to AppleScript. desktop-notifier only reaches Notification Center from
an app bundle, so a pip-installed tray uses AppleScript directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: make each tray menu action reach something real

Four menu actions opened the wrong thing or nothing:

- Needs attention on the byoc lane fell back to the hosted dashboard when
  the desktop app wasn't connected or refused the command. On byoc it now
  only opens Teach in OpenAdapt Desktop: the tray starts the desktop app if
  needed, and says so if that fails. The lane counts as byoc when either
  tray.json or the desktop's status report says byoc; before, a byoc lane
  reported by the desktop was ignored for routing.
- Recent Captures > View ran `openadapt visualize`, which the current
  launcher doesn't have, and waited for it to fail before opening the
  folder. View now opens the folder and reports a file browser that didn't.
- Settings... opened http://localhost:<dashboard_port>/settings, a page from
  the pre-1.0 dashboard that nothing serves. It now opens tray.json in a text
  editor (open -t, xdg-open, or Notepad), writing the defaults first if the
  file is missing and never rewriting an existing one.
- Pause Sync set the sync channel to "synced" and the menu kept offering
  Pause Sync, so a pause couldn't be undone from the tray. The tray now
  tracks a paused state, including the desktop's "paused" sync_state event,
  and offers Resume Sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: lead the README with what the tray does and a checked quick start

The first screen now says what the tray does, who it's for, and the two
commands to start it. The socket and cloud details move to
docs/PROTOCOL.md, linked once. The scattered "what this doesn't do yet"
caveats become one Limits section; the items that were bugs (the byoc
dashboard fallback, the legacy View command) are fixed in the previous
commits instead.

The PyPI summary drops "governed repair" for a plain description, and the
metadata test pins the new claims and rejects internal process words
(admission, signed ledger, governed, and so on) in the README and summary.

The quick start was run from a wheel built from this branch in an empty
Python 3.12 venv on macOS 15.7.3: the tray stays up and its menu matches the
README. Links use absolute URLs so they also work on PyPI, and twine check
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: state what the quick start can reach today

A first-time run of the quick start turned up five gaps between the README
and the code:

- The quick start linked to OpenAdapt Desktop's README "for which build to
  use", but that README says there is no trusted build to download. The
  tray also only launches the native app; the `openadapt-desktop` command
  that pip installs never opens the socket the tray connects to. The quick
  start now says both.
- A new user with no cloud token sees "Sync (offline)", greyed out, not
  "Pause Sync". The menu listing now shows it.
- On macOS a pip-installed tray sends notifications through AppleScript,
  which has no click callback, so clicking a needs-attention notification
  can't open the dashboard. The README no longer says it does, and Limits
  says so.
- Settings... writes seven keys the tray never reads (auto_start_on_login,
  default_record_audio, and others). The README names them.
- pynput's macOS keyboard listener uses a listen-only event tap, which
  needs Input Monitoring as well as Accessibility.

Also: say what the hand check on macOS covered (start-up and the menu, not
recording or hotkeys), explain `byoc` by what it does, describe compiling
on first mention, list the three modules the layout skipped, and replace
"control plane" with OpenAdapt Cloud in PROTOCOL.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The hotkey label tests build only the recording item, from a mocked
config. Nothing checked the whole menu that a fresh install shows, so a
raw pynput string in another item, or a returning "Login..." item, would
pass.

The new test builds the full idle menu from the real default TrayConfig
on macOS, Windows and Linux, for three account states. It checks that
the first item reads "Start Recording (⌃⇧R)" or
"Start Recording (Ctrl+Shift+R)", that no label contains "<" or ">", that
no label starts with "Login", that an "Account: " item is present, and
that the stored setting keeps pynput's syntax.

It fails on the code before #69 and passes now. It also catches an added
"Login..." item and a raw hotkey in another item, which the existing
tests miss.

Ledger: B121

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Upgrade the two vulnerable packages in the release tool lock
(scripts/release/uv.lock) to their first patched versions:

- urllib3 2.7.0 -> 2.8.0 (GHSA-8988-9cw3-xx77, GHSA-vxq7-64xx-v4gw,
  GHSA-gh4c-6fx4-qh6g). Transitive, through requests.
- uv 0.12.13 -> 0.12.18 (GHSA-2cv4-cqwr-gwf7). Pinned in the release
  dependency group.

The release workflow installs uv through setup-uv with the same pinned
version, and the release contract test asserts both pins, so all three
move together. No other package in either lock changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bumps the codeql-action group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@b96794f...1c5b675)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@b96794f...1c5b675)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…7.0 (#68)

Bumps [python-semantic-release/publish-action](https://github.com/python-semantic-release/publish-action) from 10.6.2 to 10.7.0.
- [Release notes](https://github.com/python-semantic-release/publish-action/releases)
- [Changelog](https://github.com/python-semantic-release/publish-action/blob/main/releaserc.toml)
- [Commits](python-semantic-release/publish-action@6d89e2d...99d4062)

---
updated-dependencies:
- dependency-name: python-semantic-release/publish-action
  dependency-version: 10.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.1.0 to 10.3.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@bec219d...1c37ad0)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore: bump hatchling from 1.32.0 to 1.32.4

Bumps [hatchling](https://github.com/pypa/hatch) from 1.32.0 to 1.32.4.
- [Release notes](https://github.com/pypa/hatch/releases)
- [Commits](pypa/hatch@hatchling-v1.32.0...hatchling-v1.32.4)

---
updated-dependencies:
- dependency-name: hatchling
  dependency-version: 1.32.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: align release-tool hatchling pin and contract with 1.32.4

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: abrichr <richard.abrich@mldsai.com>
* chore: update ruff requirement from <0.17,>=0.16 to >=0.16,<0.18

Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.0...0.17.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.17.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: refresh uv.lock for ruff range

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: abrichr <richard.abrich@mldsai.com>
Imports OpenAdaptAI/openadapt-tray main at e979dbbf77439240daac7df0d33d708e7d0cf2c6
(82 commits, v0.3.7) with its history, rewritten by
git filter-repo --to-subdirectory-filter packages/tray --preserve-commit-hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the standalone release machinery (semantic-release, its locked tool
project and wrapper, and the repository's .github workflows). Releases now go
through the root Release package workflow, like privacy. The release-contract
test keeps the version and lock checks and adds the changelog and workflow
checks. The changelog is backfilled from the v0.2.1..v0.3.7 GitHub Releases;
their commit links still resolve in OpenAdaptAI/openadapt-tray. README,
PROTOCOL.md and the project URLs point at packages/tray.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
packages.yml gets a tray scope: tests on Linux 3.10 and 3.12 for a PR (plus
3.11, macOS and Windows on main), a lint and release-contract job, and
Desktop's socket test against this tray client. All three are in
packages-gate. desktop-test.yml no longer clones openadapt-tray at a pinned
commit; it reads packages/tray/src. release-package.yml can publish
openadapt-tray through pypi-tray, and verify_release_hosting accepts
openadapt-tray-v* tags. Dependabot watches packages/tray/uv.lock. The commit
map, README package table, CAPABILITIES, CONTRIBUTING, CLAUDE.md and the
tray design doc are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr and others added 3 commits October 19, 2026 14:59
desktop-test.yml now reads packages/tray/src, so the qualification test
asserts that no step checks out another repository and that the tray IPC
client exists at OPENADAPT_TRAY_SOURCE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant