Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 107 additions & 197 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,104 +1,37 @@
# openadapt-tray

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
[![PyPI version](https://img.shields.io/pypi/v/openadapt-tray.svg)](https://pypi.org/project/openadapt-tray/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

> **Lifecycle: Experimental supporting surface.** The canonical compiler and
> governed runtime live in
> [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow). This tray
> is published on PyPI as `openadapt-tray`, but it is a companion status
> surface, not a generally available integrated desktop product.

## What OpenAdapt is

OpenAdapt is a governed demonstration compiler. You record a workflow once, it
compiles the demonstration into a deterministic program, and it replays that
program with **zero model calls on the healthy path**. When an interface drifts,
OpenAdapt re-resolves from retained evidence or proposes a governed repair, and
it **halts instead of guessing** when verification fails. Substrates are all
first-class in the product design (web, Windows, macOS, Linux, RDP, and
Citrix/VDI), with browser proven end to end today and the other substrates at
earlier maturity. That workflow logic belongs to `openadapt-flow`.

## What the tray is

OpenAdapt Tray is a lightweight system-tray companion for the OpenAdapt desktop
authoring experience. It does not record, compile, replay, repair, or train
anything itself. It mirrors state and hands local actions to a companion desktop
process over an authenticated loopback connection, and it reads a small
needs-attention count from the hosted control plane.

From the tray you can:

- **Start or stop recording** on the desktop app, and see the live recording
and compile lifecycle.
- **Open recent captures** from the local captures directory.
- **See how many automations need attention** and jump straight to them.
- **Open the desktop app** or the **cloud dashboard**.
- **Pause or resume sync**, shown as its own channel.
- **Sign in** (open the ingest-token settings page) and open **settings**.

### The per-state OpenAdapt-mark icon

The tray icon is always the OpenAdapt mark, tinted per lifecycle state so the
state stays readable at a glance:

| State | Tint |
| --- | --- |
| Idle | Brand blue |
| Recording starting | Amber |
| Recording | Red |
| Recording stopping | Amber |
| Compiling | Purple |
| Error | Red |

The mark is stored once as a high-resolution transparent master and tinted from
it, so every state renders as the same recognizable mark and the states never
drift apart. Recording lifecycle and sync are modelled as two independent
channels: a machine can be compiling a fresh recording while a previous push is
still syncing, or sit idle while offline.

## How it fits with desktop and cloud
A menu-bar icon that shows what OpenAdapt is doing and gives you a start/stop
button for it. It records nothing, compiles nothing, and replays nothing. It
mirrors state from the desktop app over an authenticated loopback socket and
reads one number from the hosted control plane.

```text
openadapt-tray (status mirror + launcher)
| \
| authenticated loopback IPC \ HTTPS: GET needs-attention count
v v
openadapt-desktop (local cockpit) hosted control plane (app.openadapt.ai)
|
v
openadapt-flow (compile / replay / halt / repair / teach)
```
Which means the honest description is: this is a status surface, not an
integrated desktop product. The workflow logic all lives in
[`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow).

- **Local control** goes to `openadapt-desktop` over an authenticated loopback
socket. The desktop app writes a discovery file the tray reads, then the tray
sends start/stop, open-library, open-teach, and pause/resume-sync commands and
consumes desktop status events.
- **Hosted status** is a narrow read: the tray polls a needs-attention count and
routes a click to the right place. It never uploads screenshots, bundles, or
capture artifacts.
[Documentation](https://docs.openadapt.ai) ·
[openadapt-desktop](https://github.com/OpenAdaptAI/openadapt-desktop) ·
[OpenAdapt launcher](https://github.com/OpenAdaptAI/OpenAdapt)

## Release boundary
## Install

- The hosted-lifecycle behavior described here is merged on `main` and published
to PyPI; the package classifier is Pre-Alpha.
- Unit tests cover the client state machine, IPC framing, menus, the per-state
icon tinting, and mocked hosted HTTP behavior. They do not prove a working
desktop installer, a live hosted service, or an end-to-end authoring loop.
- The `openadapt-desktop` `main` branch now serves the exact discovery-socket
and command contract this tray expects, but the two surfaces have
not been validated together end to end, and no signed, generally available
desktop build ships that server yet. Treat the tray as a status surface until
that integration is qualified.
```bash
pip install openadapt-tray
openadapt-tray # openadapt-tray-gui is the same entry point,
# registered under gui_scripts so Windows
# launches it without a console window
```

The retired model-training controls and training states are not part of this
release.
Needs a graphical session. With no desktop IPC server and no hosted token it
will sit there offline, which is correct behaviour rather than a failure.

## Expected menu
## The menu

The menu is built from current local and hosted state:
Built from whatever local and hosted state currently exists:

```text
Start Recording (<configured hotkey>)
Expand All @@ -112,30 +45,40 @@ Settings...
Quit
```

The account row changes to a sign-in action, an expiry warning, or an
unavailable status. Selecting it opens the credential settings page.

During a local operation, the recording item changes to Starting, Stop
Recording, Stopping, or Compiling. These labels reflect events; the tray does
not perform the work.
During a local operation the recording item becomes Starting, Stop Recording,
Stopping, or Compiling. The account row turns into a sign-in action, an expiry
warning, or an unavailable status, and clicking it opens the credential
settings page. These labels follow events; the tray isn't doing the work.

## Integration contract
The icon is always the OpenAdapt mark, tinted by state so you can read it at a
glance: brand blue idle, amber starting or stopping, red while recording,
purple while compiling, red on error. One high-resolution transparent master
gets tinted, so the states can't drift into different-looking marks.

### Local desktop IPC
Recording and sync are two independent channels. A machine can compile a fresh
recording while a previous push is still syncing, or sit idle and offline.

The tray discovers a local service from `~/.openadapt/desktop_ipc.json`, then
uses an authenticated loopback connection. It can send commands to start or stop
recording, open the workflow library or teach surface, and pause or resume sync.
It also consumes desktop status events.
## How it talks to things

If discovery fails, the tray launches `openadapt-desktop` and waits about ten
seconds for the service. The desktop `main` branch now implements a matching
token-authenticated loopback server and writes this discovery file, but that
path has not yet been validated end to end from a shipped desktop build.
```text
openadapt-tray (status mirror + launcher)
| \
| authenticated loopback IPC \ HTTPS: GET needs-attention count
v v
openadapt-desktop (local cockpit) hosted control plane (app.openadapt.ai)
|
v
openadapt-flow (compile / replay / halt / repair / teach)
```

### Hosted needs-attention polling
Local control goes to `openadapt-desktop`. The desktop app writes a discovery
file at `~/.openadapt/desktop_ipc.json`, the tray reads it and opens an
authenticated loopback connection, then sends start/stop, open-library,
open-teach, and pause/resume-sync, and consumes status events coming back. If
discovery fails, the tray launches `openadapt-desktop` and waits about ten
seconds.

The poller calls:
Hosted state is one narrow read:

```text
GET <hosted_url>/api/needs-attention/count
Expand All @@ -149,70 +92,47 @@ Authorization: Bearer <ingest token>
} }
```

The token is resolved from `OPENADAPT_INGEST_TOKEN` or the OS keychain and is
not written to `tray.json`. The default poll interval is 60 seconds, clamped to
at least 30 seconds, with a slower offline retry.

The control plane decides when the credential enters its 14-day warning
window. The tray shows one actionable notification for each credential and
expiry, including after a tray restart. It stores only a non-secret identity
digest for notification deduplication. It never stores or logs the token.

This is a narrow status endpoint, not hosted execution. The tray does not upload
screenshots, workflow bundles, or capture artifacts through this poller.

### Deployment-lane routing

- `cloud`: a needs-attention click opens `<hosted_url>/dashboard`, which lists
open halts and uncertain dispatches.
- `byoc`: while desktop IPC is connected, the click sends `open_teach` locally
so workflow data can remain in the customer environment.
- `byoc` without desktop IPC: the current implementation falls back to the
hosted dashboard. Regulated deployments must not treat this fallback as a
validated PHI-safe path; it should be changed or policy-gated before
production use.

## Installation

```bash
pip install openadapt-tray
openadapt-tray # run the tray application
```

Treat the install as an Experimental status surface, not a production desktop
product (see the release boundary above).

## Development quickstart

```bash
git clone https://github.com/OpenAdaptAI/openadapt-tray.git
cd openadapt-tray

uv sync --extra dev
uv run pytest tests -q
uv run openadapt-tray
```

Running the process requires a graphical desktop/session. Without a compatible
desktop IPC server or hosted token it may correctly remain offline, fail local
actions, or open only browser routes.

For a runnable OpenAdapt workflow, use the canonical launcher separately:

```bash
pip install 'openadapt[browser]'
openadapt quickstart
```
The token comes from `OPENADAPT_INGEST_TOKEN` or the OS keychain, never from
`tray.json`, and it's never stored or logged. Polling defaults to 60 seconds,
clamps to a 30-second floor, and backs off when offline. The control plane
decides when a credential enters its 14-day warning window; the tray shows one
actionable notification per credential and expiry, surviving a tray restart,
and keeps only a non-secret identity digest to deduplicate.

No screenshots, bundles, or capture artifacts go through this poller.

Clicking the needs-attention row routes by lane. On `cloud` it opens
`<hosted_url>/dashboard`. On `byoc` with desktop IPC connected it sends
`open_teach` locally, so workflow data stays inside the customer environment.
On `byoc` with no desktop IPC it currently falls back to the hosted dashboard,
and a regulated deployment should not treat that fallback as a validated
PHI-safe path. Gate it or change it before production.

## What this doesn't do yet

The tray's client behaviour is covered by unit tests: the state machine, the
IPC framing, the menus, the icon tinting, and mocked hosted HTTP. None of that
proves a working installer, a live hosted service, or an authoring loop that
runs end to end.

- `openadapt-desktop`'s `main` serves the exact discovery-socket and command
contract this tray expects, but the two have not been validated together end
to end, and no signed generally-available desktop build ships that server.
- No packaged installer proves tray startup, permissions, or auto-start on
macOS, Windows, and Linux.
- Hosted polling is tested against mocks. Nothing here validates a live service
contract.
- Recent-capture View still calls a legacy launcher command before falling back
to a file browser.
- Sign-in opens a settings page. There's no interactive authentication.
- The tray doesn't certify a workflow or verify its effects. Nothing here is a
safety control.

## Configuration

Non-secret settings are stored at:

- macOS: `~/Library/Application Support/openadapt/tray.json`
- Windows: `%APPDATA%/openadapt/tray.json`
- Linux: `${XDG_CONFIG_HOME:-~/.config}/openadapt/tray.json`

Representative settings:
Non-secret settings live at `~/Library/Application Support/openadapt/tray.json`
on macOS, `%APPDATA%/openadapt/tray.json` on Windows, and
`${XDG_CONFIG_HOME:-~/.config}/openadapt/tray.json` on Linux:

```json
{
Expand All @@ -231,25 +151,20 @@ Representative settings:
}
```

`deployment_lane` accepts `cloud` or `byoc`. The ingest token does not belong in
this file.
`deployment_lane` takes `cloud` or `byoc`. The triple-ctrl stop hotkey is real,
not a typo: the `stop_on_triple_ctrl` setting defaults to true and is what
enables it. The ingest token does
not belong in this file.

## Known gaps
## Development

- The desktop socket contract exists on the desktop `main` branch, but no
shipped desktop build has been validated end to end with this tray.
- No packaged installer proves tray startup, permissions, or auto-start across
macOS, Windows, and Linux.
- Hosted polling is tested with mocks; repository tests do not validate a live
service contract or service-level commitments.
- BYOC fallback can open the hosted dashboard when desktop IPC is absent.
- Recent-capture View still invokes a legacy launcher command before its
file-browser fallback.
- Login opens an ingest-token settings page; the tray does not implement
interactive authentication.
- The tray does not certify workflow safety or verify workflow effects.

## Project structure
```bash
git clone https://github.com/OpenAdaptAI/openadapt-tray.git
cd openadapt-tray
uv sync --extra dev
uv run pytest tests -q
uv run openadapt-tray
```

```text
src/openadapt_tray/
Expand All @@ -261,20 +176,15 @@ src/openadapt_tray/
icons.py per-state OpenAdapt-mark tinting
keychain.py ingest-token lookup
config.py non-secret local preferences
tests/ mocked/unit coverage for these client boundaries
```

## Related projects

| Project | Lifecycle and role |
| --- | --- |
| [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow) | Canonical workflow compiler, runtime, certification, and governed repair engine |
| [`openadapt-desktop`](https://github.com/OpenAdaptAI/openadapt-desktop) | Experimental authoring/teaching cockpit; its `main` now serves the IPC contract this tray expects, pending end-to-end validation |
| [`OpenAdapt`](https://github.com/OpenAdaptAI/OpenAdapt) | Flagship launcher and meta-repository |
For an OpenAdapt workflow you can actually run, use the launcher instead:

Documentation for the wider stack lives at
[docs.openadapt.ai](https://docs.openadapt.ai).
```bash
pip install 'openadapt[browser]'
openadapt quickstart
```

## License

MIT. See [LICENSE](LICENSE).
[MIT](LICENSE)
18 changes: 10 additions & 8 deletions tests/test_public_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@


def test_public_metadata_identifies_unreleased_supporting_surface() -> None:
readme = (ROOT / "README.md").read_text()
# Collapse wrapping so an assertion matches the sentence rather than the
# line breaks a reflow happens to leave behind.
readme = re.sub(r"\s+", " ", (ROOT / "README.md").read_text())
pyproject = (ROOT / "pyproject.toml").read_text()

assert "Lifecycle: Experimental supporting surface" in readme
# The hosted lifecycle shipped on PyPI and the openadapt-desktop main branch
# now serves the matching IPC contract, but the two surfaces have not been
# validated together end to end and no signed desktop build ships that
# server. The README must keep saying so rather than implying an integrated,
# generally available product.
assert "Release boundary" in readme
# The README must keep saying that this package is a status surface rather
# than an integrated, generally available desktop product, and it must keep
# saying that the tray and openadapt-desktop have not been proven together.
# These assert the claims, not one particular heading, so the wording can
# improve without weakening the guard.
assert "status surface, not an integrated desktop product" in readme
assert "records nothing, compiles nothing, and replays nothing" in readme
assert "not been validated together end to end" in readme
assert "openadapt-flow" in readme
assert "Development Status :: 2 - Pre-Alpha" in pyproject
Expand Down