fix: stop flashing the permission gate and empty state at startup#212
Conversation
…changes An app update (Homebrew cask, the in-app updater, a dev rebuild) replaces the bundle on disk while the old agent keeps running: launchd only restarts the process when it exits, so the stale agent would serve until the next login. Once the IPC protocol version moves, a GUI from the new bundle then refuses the old agent and has no way forward. Watch our own executable (one stat every 10s) and exec the new image when it changes. exec keeps the pid, needs no respawner — so remapping continues even with autostart off and no GUI — and leaves the KeepAlive SuccessfulExit semantics (tray Quit stays final) untouched. Windows has no exec; exit cleanly there and let the GUI's socket-down spawn retry or the next login start the replacement.
… status A freshly-(re)started agent serves an empty inventory until its own first HID enumeration completes, and the GUI had no way to tell that apart from a genuinely empty device set — it cleared its "Scanning…" state on the first poll and declared "No devices connected" at users whose devices were about to appear. AgentStatus gains inventory_ready, flipped by the orchestrator's first refresh_inventory (an empty snapshot still counts: the watcher only forwards completed enumerations). Protocol version bumps to 2 — the bincode layout changed — which is now safe to ship: the previous commit's self-restart converges a stale running agent onto the new binary within seconds of an app update.
…apshot On launch the window opened on AppState's assumed defaults — accessibility not granted, no devices — and corrected itself only when the first IPC poll landed, flashing the permission gate and then the empty state at every already-authorized user with paired devices. accessibility_granted becomes Option<bool> (None until the agent has actually answered); render holds a neutral spinner + "Connecting to the background service…" frame while it's None, which is also the honest screen when the agent is genuinely unreachable. scanning now tracks the agent's inventory_ready instead of latching false on the first poll, so a fresh agent's empty snapshot reads "Scanning for devices…", not "No devices connected". The Settings permission row maps the unknown state to PermissionStatus::Unknown instead of pretending it was denied. The new string ships in all 21 locales at the same ordered position.
Greptile SummaryThis PR eliminates two visible startup flashes — the accessibility permission gate and the "No devices connected" empty state — by deferring UI decisions until real agent data arrives. It also adds a self-restart mechanism so an updated agent binary picks itself up within one 10-second tick, keeping protocol compatibility without requiring a logout.
Confidence Score: 5/5Safe to merge — all changed paths have correct fallbacks and the previous argv-forwarding concern has been addressed. The three interlocking pieces (Option accessibility state, inventory_ready IPC field, self-restart watcher) are each internally consistent and compose correctly. All edge cases are handled: vanishing file mid-replace returns None and skips the tick; exec failure exits with code 1 for launchd respawn; thread-spawn failure degrades gracefully; empty first enumeration still sets inventory_ready. No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "fix(agent): forward argv across the self..." | Re-trigger Greptile |
The agent takes no arguments today, but a future flag would silently vanish across a binary-update restart; pass our own argv through.
…t handling Several poll-loop fixes from the post-merge review of #212/#213: - poll() now fetches status BEFORE inventory. The other way around, the agent's first enumeration could land between the two RPCs and pair an empty pre-enumeration inventory with ready=true — re-creating the 'No devices connected' flash the fast poll exists to prevent, stretched to a full steady period by the cadence latch. The inverse pairing is benign (devices render regardless of the scanning state). - Cadence policy extracted into a unit-tested Pacing type, and the command-branch disconnect now re-arms the fast cadence too — it previously only reset client, contradicting the 'back to fast whenever the connection is lost' contract, so a disconnect detected by an in-flight command re-converged at 2 s instead of 250 ms. - The fast phase is capped (15 s without readiness → steady): an agent that never becomes ready, or a protocol mismatch, no longer holds the loop (and its connect+warn round) at 4 Hz for the GUI's lifetime. - New GuiUpdate signals: Unreachable (no snapshot 15 s into an outage) and OutdatedGui (agent speaks a newer protocol) now reach AppState, wiring up the static frames from the AgentLink commit — no more eternal connecting spinner, and no frozen live-looking UI after an in-place app update. - A pairing session interrupted by an agent restart synthesizes a Failed event (the agent's terminal-event guarantee dies with the process), so the Add Device window no longer sits in 'Searching…' until manually cancelled. - Both intervals use MissedTickBehavior::Delay (a stalled poll no longer bursts its missed ticks), the spawn gate skips the tick that detected a drop (don't race the agent's self-exec for the singleton lock), identical snapshots skip the full-window refresh, and the module docs drop the stale 'launchd KeepAlive reconnects us' claim.
Problem
On every launch the main window briefly flashes two screens that are both false for a set-up user:
AppStatestarts withaccessibility_granted: falseand the window opens before the first agent IPC poll lands, so the gate renders for the first frames of every single launch.scanningunconditionally, but a freshly-(re)started agent serves an empty inventory until its own first HID enumeration completes, so users see "No devices connected" while their devices are about to appear.Fix
GUI —
accessibility_grantedbecomesOption<bool>; until the first agent snapshot arrives the window holds a neutral connecting frame (spinner + "Connecting to the background service…") instead of rendering states it can't know yet. That frame is also the honest screen while the agent is genuinely unreachable. The Settings permission row maps the unknown state toUnknownrather than pretending denial.IPC (protocol v2) —
AgentStatusgainsinventory_ready, set by the orchestrator's firstrefresh_inventory(an empty snapshot still counts — the watcher only forwards completed enumerations). The GUI'sscanningnow tracks it, so a fresh agent's empty list reads "Scanning for devices…", not "No devices connected".Agent self-restart — what makes the first-ever protocol bump shippable: an app update replaces the bundle while the old agent keeps running, and launchd only restarts it on exit, so a v2 GUI would refuse the stale v1 agent and sit on the connecting screen until logout. The agent now stats its own executable every 10 s and
execs the new image when it changes — same pid, no respawner needed (covers autostart-off + GUI-closed setups), and theSuccessfulExit: false/ tray-Quit semantics stay intact. Windows (noexec, no shipped agent today) exits cleanly and relies on the GUI's spawn retry or next login.New i18n string added to all 21 locales at the same ordered position.
Startup now
Verification
cargo test(incl. newempty_refresh_marks_inventory_ready), full-workspace clippy, and the windows-gnu cross-lint all pass.