* feat(workers): join from the app, share by QR, and a status-bar Compute control
Remote workers shipped with a hole in the middle: the control plane could
mint join codes, and on the other machine there was nothing to paste them
into. Becoming a worker meant launching with OMNIVOICE_WORKER_MODE and
OMNIVOICE_WORKER_TOKEN in the environment and relaunching — on the machine
that is usually the least convenient one to configure by hand.
Backend
- GET /workers/agent, POST /workers/agent/join, POST /workers/agent/enabled.
Join redeems a code and starts the agent live; no restart.
- Worker mode now persists in settings as well as the environment (env still
wins, and the panel is told so it can disable a switch it cannot honour),
and it is written only after a join that actually worked — a failed
enrolment must not have the app retrying on every launch.
- The endpoint carried by the redeemed code is remembered. Without that a
machine that joined from the UI came back up enrolled but with nowhere to
dial, and the only fix was OMNIVOICE_WORKER_ENDPOINT.
UI
- "Lend this machine's GPU": paste the code, Join. Once joined it offers a
switch rather than another code, because the pinned certificate survives.
- <OneTimeSecret/> renders join codes and connection strings as a QR next to
the text, with a live expiry countdown, and is used by both halves. QR
generation is best-effort: a string past the format's capacity still shows
the code and Copy, because losing the QR is a degraded share and losing the
only copy of a one-time secret is data loss.
- Status-bar Compute control: pick local or a machine, flip the feature, mint
a join code — without opening Settings. Absent entirely until the user has
opted in or enrolled something.
- Remote workers now reads as a device list: status dot, address, latency,
live task meter, resident models, last seen; housekeeping actions revealed
on hover; a three-step empty state.
- Approve is on the row. A worker could connect, sit there labelled "Not
approved" and never be usable, with no way out of it in the UI.
Fixes found on the way
- Status dots and menu surfaces in the GPU picker were painted from fixed
Tailwind palette classes (bg-emerald-400, text-amber-400, hover:bg-white/5),
so on Midnight or Catppuccin they showed Gruvbox colours next to the
theme's own. Both controls now paint from themed --color-* tokens, shared
in computeTarget.jsx along with the JSON wrapper all three copies duplicated.
- Button funnels every child into one <span>, so an icon passed as a child
renders glued to its label — the flex gap only applies to the `leading`
slot. Six buttons across these panels were affected.
- InboundNodePanel passed `variant="warning"` to Badge, which takes `tone`;
the "on your network" warning rendered as an ordinary neutral pill.
Docs updated in the same change (docs/remote-workers.md): the join flow, the
QR, the status-bar control, and the new environment variable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(changelog): stamp the remote-workers entries with their PR ref
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(workers): a join is not done until the control plane accepts it
Review findings on #1516:
- Greptile P1: `start()` only SCHEDULES the dial-out loop, so a control
plane that rejected this worker — expired token, wrong address, a
server that never answers — looked identical to a successful join. The
route persisted worker mode, reported success, and the machine retried
forever on every launch. The agent now signals first registration, and
join waits for it before persisting anything.
- CodeRabbit: a failed REJOIN left the machine unable to reconnect to the
control plane it was already serving, because pinning the new
certificate overwrites the old one on disk. Snapshot the pinned
certificate, endpoint and setting up front, and restore them (and the
running agent) when the join fails.
- CodeRabbit: join and the enable toggle awaited stop()/start() with no
exclusion, so two concurrent requests could interleave their pairs and
have `start()` return early — reporting success for a control plane it
never dialled. Both now hold one lifecycle lock.
- CodeRabbit: with OMNIVOICE_WORKER_MODE set, the toggle still started or
stopped the agent and wrote a setting the rest of the app ignores,
contradicting the env_pinned status it reports. It now answers 409 and
says which variable is in charge.
- CodeRabbit: the QR code kept encoding the previous secret until the new
one finished encoding, so the code on screen could disagree with the
text beside it.
CI: regenerated tests/fixtures/api_routes.txt for the three
/workers/agent routes.
Tests: a join the control plane never accepts is a 409 that persists
nothing and leaves no agent dialling; a failed rejoin restores the
previous certificate, endpoint and setting; an env-pinned machine
refuses the toggle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(workers): the environment pin governs joining too, not just the toggle
CodeRabbit, #1516:
- join_control_plane skipped the env_pinned guard set_agent_enabled
enforces, and joining is precisely what ENABLES worker mode: under
OMNIVOICE_WORKER_MODE it wrote a setting nothing consults, and with the
variable pinned off it handed back a machine that reported a successful
join and lent nothing. One shared guard now covers both routes.
- Two of the three rollback assertions could not fail before the fix
(nothing wrote those settings on the failure path). The test now pins
the behaviour only the rollback produces: the previous enrollment is
dialling again, rather than left stopped until someone notices.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>