Skip to content

feat(tray): show local recorders and record without Desktop - #1481

Draft
abrichr wants to merge 2 commits into
mainfrom
feat/tray-local-recorders
Draft

abrichr wants to merge 2 commits into
mainfrom
feat/tray-local-recorders

Conversation

@abrichr

@abrichr abrichr commented Oct 21, 2026

Copy link
Copy Markdown
Member

Why

The tray only mirrored OpenAdapt Desktop. Desktop has no signed build, so on a Mac without it, a recording started with openadapt-flow record gave no cue in the menu bar. The person teaching could not tell when the recorder was actually capturing.

openadapt-flow record prints "Perform your workflow on the target desktop now" before the recorder has started (packages/flow/openadapt_flow/desktop_record.py, the announce block runs before with factory(...), and the result of wait_for_ready is ignored). So the terminal text is not a readiness signal.

What changes

  • capture_watch.py: a polling watcher (0.5 s) over openadapt-capture's public client contract: discover_recorders() and status_recording(). Stages: starting → ready (only when the recorder reports phase == "recording" and ready: true) → stopping (stopping/finalizing) → ended. When the recorder's descriptor goes away, the outcome comes from capture-state.json. Only a file for that exact session that is complete and integrity_verified counts as saved. A missing or mismatched file is reported as "result could not be read", never as saved.
  • local_record.py (macOS only): when Desktop is not reachable, Start Recording runs openadapt-flow record --backend macos --out <captures_directory>/<name> --task <name> in its own session, with output going to <name>.record.log. Stop Recording sends SIGINT, the Ctrl-C the command documents. A recording started in a terminal is stopped through stop_recording() on its control channel.
  • app.py: the watcher drives the icon, tooltip and notifications only while Desktop is not connected, so Desktop's recordings are not shown twice. While the tray runs its own openadapt-flow record, it ignores other recorders. If flow exits non-zero after the capture was saved, the tray says the capture was saved but conversion failed, with the log path.
  • Two existing bugs this exposed:
    • Any transition to IDLE notified "Recording Stopped / Capture saved", including a start that never reached recording. That was a false success, seen in the real run below. It now notifies only after RECORDING, STOPPING or COMPILING.
    • can_start_recording() was IDLE-only, so after any ERROR, Start stayed disabled until the tray restarted. ERROR is terminal, so Start now works from it.
  • Config: watch_local_recorders (default true). README section "Recording without OpenAdapt Desktop". CAPABILITIES row.

Evidence (one Mac, macOS 15, real recorder, real menu-bar icon)

Scratch env with this branch's packages/capture, packages/flow[macos] and packages/tray installed editable.

Real tray app (pystray icon and real notifications), Desktop launch disabled, Start and Stop driven through start_recording()/stop_recording(), window-scoped to Calculator:

  3.05s start_recording()
  3.10s STATE RECORDING_STARTING icon_title='OpenAdapt: starting the recorder'
        Recording with openadapt-flow into …/tray-captures/tray-selftest
  9.76s NOTIFY 'Recording Started': 'Capturing: tray-selftest'
  9.76s STATE RECORDING icon_title='OpenAdapt: recording'
 13.64s stop_recording()
 13.65s STATE RECORDING_STOPPING icon_title='OpenAdapt: saving the recording'
 16.96s NOTIFY 'Recording Stopped': 'Saved: tray-selftest'
 17.15s NOTIFY 'Error': 'The capture was saved, but openadapt-flow could not convert it (exit code 1). See …/tray-selftest.record.log.'

The recorder was ready 6.6 s after the command started. The conversion error is expected: no clicks were made, and flow refuses a capture with no actions.

Watcher alone vs. a terminal-style openadapt-flow record:

Run Watcher saw Recorder's own terminal state
Full desktop (3024x1964) starting 3.6 s → stopping 34.5 s → ended failed (recording_failed). Never ready video_writer readiness timeout at 30 s, the same failure seen in the sign-in spike
--macos-app Finder starting → ended failed (window_target_ambiguous) 29 matching windows
--macos-app Calculator --macos-window-title Calculator starting 3.4 s → ready 9.1 s → SIGINT → stopping → ended complete phase: complete, complete: true, integrity_verified: true

In the full-desktop case, flow had already told the person to perform the workflow, but the recorder never became ready. The tray stayed amber ("starting the recorder") and then showed the failure.

Tests: uv run pytest -q → 373 passed, 2 skipped. ruff check src tests is clean (the new files are ruff format clean; the package as a whole is not yet).

Open questions

  1. openadapt-capture version. The watcher needs openadapt-capture ≥ 1.3.0 (control channel). openadapt-flow[capture] still allows >=1.2.0, and an env with capture 1.2.2 silently gets no watcher (the tray prints one line at start). Should flow's capture extra require >=1.3.0? And should the tray get a capture extra (openadapt-capture>=1.3.0)? I left both out to avoid a large uv.lock change in this PR.
  2. A Desktop build registered with LaunchServices. On this Mac, open -b ai.openadapt.desktop launched a local development build of Desktop. The tray prefers Desktop when it can launch it, so Start went to that build (which then failed on Input Monitoring) instead of to openadapt-flow. Should the tray prefer openadapt-flow when Desktop isn't already running, or offer a setting?
  3. Stopping a terminal openadapt-flow record from the tray. The control-channel stop finalizes the capture, but flow's _wait_for_stop only returns on Ctrl-C or --until-cmd. So flow keeps waiting until Ctrl-C in its terminal, and only then converts. A small flow change (return when the recorder stops) would fix it. Follow-up PR?
  4. Which recording to show. Other local recorders also appear: during this run, another process's test recorder (failed-finalization) was visible. The tray shows the first one it sees, or only its own while it runs openadapt-flow record. Should test recorders use their own OPENADAPT_CAPTURE_RUNTIME_DIR so they never reach a person's tray?
  5. Spoken cue. The sign-in spike used a spoken cue as well as a notification. Add an opt-in say on ready/stop (macOS), or is the icon plus notification enough?
  6. Windows and Linux. The watcher is cross-platform, but tray-started recording is macOS-only. On Windows, flow stops on Ctrl-C, which a tray cannot send reliably to a separate console process. Linux needs --linux-allow-physical-input. Worth doing, and how should the tray stop the recorder there (control channel plus the flow fix from question 3)?
  7. Backend arguments. Tray-started recordings use flow's default full-desktop macOS capture, which timed out at 30 s on this 3024x1964 display. Should the tray prompt for an app or window, or remember one in tray.json?

Nothing is released. Releases go only through the reviewed workflow.

🤖 Generated with Claude Code

abrichr and others added 2 commits October 20, 2026 23:21
The tray only mirrored OpenAdapt Desktop, so a recording started with
`openadapt-flow record` or `capture record` gave no cue in the menu bar.

- Watch local Capture recorders through openadapt-capture's
  authenticated control client (discover_recorders/status_recording).
  "Recording" comes from the recorder's `ready` flag. The end comes
  from capture-state.json, and only a complete, verified session for
  that exact session counts as saved.
- On macOS, Start Recording without Desktop runs
  `openadapt-flow record --backend macos`; Stop sends it Ctrl-C. A
  recording started elsewhere is stopped through its control channel.
- Stop announcing "Capture saved" when a start never reached
  recording, and let Start work again after an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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