Skip to content

fix: stop flashing the permission gate and empty state at startup#212

Merged
AprilNEA merged 4 commits into
masterfrom
fix/startup-loading-flash
Jun 11, 2026
Merged

fix: stop flashing the permission gate and empty state at startup#212
AprilNEA merged 4 commits into
masterfrom
fix/startup-loading-flash

Conversation

@AprilNEA

@AprilNEA AprilNEA commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Problem

On every launch the main window briefly flashes two screens that are both false for a set-up user:

  1. "Accessibility permission required"AppState starts with accessibility_granted: false and the window opens before the first agent IPC poll lands, so the gate renders for the first frames of every single launch.
  2. "No devices connected" — the first poll cleared scanning unconditionally, 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

GUIaccessibility_granted becomes Option<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 to Unknown rather than pretending denial.

IPC (protocol v2)AgentStatus gains inventory_ready, set by the orchestrator's first refresh_inventory (an empty snapshot still counts — the watcher only forwards completed enumerations). The GUI's scanning now 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 the SuccessfulExit: false / tray-Quit semantics stay intact. Windows (no exec, 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

Agent state Before After
running, devices paired gate flash → empty flash → gallery connecting (≪1 s) → gallery
just (re)started gate flash → "No devices connected" connecting → "Scanning for devices…" → gallery
unreachable permanent fake gate + empty state connecting frame, honest
permission really missing gate (after flash) connecting → gate

Verification

  • cargo test (incl. new empty_refresh_marks_inventory_ready), full-workspace clippy, and the windows-gnu cross-lint all pass.
  • Ran the v2 GUI against the running v1 agent (protocol refusal): the window holds the new connecting frame — centered spinner over the plain window background with the muted localized caption ("正在连接后台服务…" under zh-CN) — no permission gate, no empty state, while the production agent keeps serving the real instance untouched.

AprilNEA added 3 commits June 11, 2026 13:39
…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-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This 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.

  • accessibility_granted changes from bool (default false) to Option<bool> (default None); a new neutral "Connecting…" spinner is rendered until the first IPC snapshot lands, replacing the pre-fix false-denial flash.
  • AgentStatus gains inventory_ready (IPC protocol bumped to v2), set by the orchestrator's first refresh_inventory call; the GUI scanning flag now tracks it so an empty post-restart inventory reads as "Scanning…" rather than "No devices".
  • self_restart::spawn() watches the agent's own executable (size + mtime fingerprint, 10 s poll) and execs the new image on Unix; on Windows it exits cleanly for the spawn-retry path.

Confidence Score: 5/5

Safe 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

Filename Overview
crates/openlogi-agent/src/self_restart.rs New file: watches own executable (size+mtime fingerprint, 10 s poll) and exec()s the new image on Unix / exits on Windows. All edge cases handled gracefully.
crates/openlogi-agent-core/src/ipc.rs Protocol bumped to v2; inventory_ready: bool added to AgentStatus with clear doc comment.
crates/openlogi-agent-core/src/orchestrator.rs Adds enumerated: bool set on first refresh_inventory call; exposes inventory_ready() accessor; unit test covers the empty-snapshot case.
crates/openlogi-agent/src/server.rs Fetches orchestrator fields in a single lock scope; includes new inventory_ready field correctly.
crates/openlogi-gui/src/state.rs accessibility_granted changed from bool to Option<bool> (default None); doc comment updated.
crates/openlogi-gui/src/app.rs Render path returns connecting_view when accessibility_granted is None; new helper is self-contained with no side effects.
crates/openlogi-gui/src/main.rs IPC update handler sets scanning = !inventory_ready and wraps accessibility_granted in Some(...).
crates/openlogi-gui/src/windows/settings.rs Three-way match maps NonePermissionStatus::Unknown instead of defaulting to Denied.

Reviews (2): Last reviewed commit: "fix(agent): forward argv across the self..." | Re-trigger Greptile

Comment thread crates/openlogi-agent/src/self_restart.rs Outdated
The agent takes no arguments today, but a future flag would silently
vanish across a binary-update restart; pass our own argv through.
@AprilNEA AprilNEA merged commit baa8093 into master Jun 11, 2026
8 checks passed
@AprilNEA AprilNEA deleted the fix/startup-loading-flash branch June 11, 2026 05:50
AprilNEA added a commit that referenced this pull request Jun 11, 2026
…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.
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