Skip to content

fix: don't flash the empty-state screen while the agent is still scanning#213

Merged
AprilNEA merged 4 commits into
masterfrom
fix/scanning-state-flash
Jun 11, 2026
Merged

fix: don't flash the empty-state screen while the agent is still scanning#213
AprilNEA merged 4 commits into
masterfrom
fix/scanning-state-flash

Conversation

@AprilNEA

Copy link
Copy Markdown
Owner

Follow-up to #212 — the startup sequence still flashed the device empty-state screen for users with paired devices.

Problem

While the agent's first enumeration is in flight (inventory_ready == false), the home body rendered the full empty-state screen — search icon, headline, Add-Device CTA, pairing hints — with only the headline swapped to "Scanning for devices…". For a user whose devices are about to appear that is still a "no devices" flash. And since the GUI polls on a 2 s timer, the gallery could lag a full period behind the agent actually knowing the devices.

Fix

  • Scanning is unknown, not empty: while scanning is set, the home body shows the same quiet spinner frame as the pre-connection view (caption "Scanning for devices…", string already in all locales). The empty-state screen with its Add-Device CTA is reserved for a completed scan that genuinely found nothing. The two loading phases share one layout (loading_body), so startup renders as a single continuous frame whose caption changes, resolving into the gallery.
  • Fast startup polling: the IPC client polls every 250 ms until the first inventory_ready snapshot arrives, then drops to the steady 2 s cadence; it falls back to fast when the connection is lost so an agent restart (binary-update exec, crash) re-converges just as quickly. A status+inventory round every 250 ms is noise for the agent.
  • Docs: recorded the tarpc/bincode invariant that method order is part of the wire format — protocol_version stays first, new methods append-only — and why the protocol deliberately has no minor version (single-bundle shipping + agent self-exec make strict equality the whole contract).

Startup now (cold, devices paired)

connecting spinner (≈agent boot) → scanning spinner (≈one enumeration, sub-second with fast polling) → gallery. No empty-state flash at any point.

Verification

cargo test, full-workspace clippy pass; the scanning frame reuses the connecting-frame layout verified in #212.

AprilNEA added 3 commits June 11, 2026 14:18
The scanning state rendered the full empty-state screen (search icon,
headline, Add-Device CTA, pairing hints) with only the headline swapped —
still a "no devices" flash for a user whose devices are about to appear.
While the agent's first enumeration is in flight the device set is unknown,
not empty: show the same quiet spinner frame as the pre-connection view
(caption "Scanning for devices…"), and reserve the empty state for a
completed scan that genuinely found nothing.
The steady 2 s poll is tuned for quiet background refresh, but at startup it
left the window on its loading frame for up to a full period after the
agent already knew the devices. Poll every 250 ms until a snapshot arrives
with inventory_ready, then drop to the steady cadence; fall back to fast on
a lost connection so an agent restart (binary-update exec, crash)
re-converges just as quickly.
…first

tarpc generates one request enum from the trait and bincode encodes the
variant index, so inserting a method shifts every later variant and breaks
even the version handshake across a skew. Document the append-only rule and
why there is deliberately no minor version: GUI and agent ship in one
bundle and the agent re-execs on binary replacement, so strict equality
plus a clean refusal is the whole contract.
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the empty-state flash during startup by separating "scan in progress" (scanning == true) from "scan complete, no devices found," so users with paired devices see a continuous spinner frame rather than a momentary add-device CTA. It also introduces fast 250 ms polling until the first inventory_ready snapshot arrives, then drops to the steady 2 s cadence.

  • app.rs: Extracts a shared loading_body helper used by both connecting_view and the new device_scanning_state; the three home-body states (has devices → gallery, scanning → spinner, empty → CTA) are now cleanly separated.
  • ipc_client.rs: Adds STARTUP_POLL_PERIOD (250 ms) with automatic promotion to the caller-supplied steady period on inventory_ready, and reversion to fast on poll-detected connection loss; the poll return type is widened to Ok(Some(inventory_ready)) / Ok(None) / Err to carry readiness without a separate channel.
  • ipc.rs: Documentation-only addition recording the tarpc/bincode wire-format invariant (method order, append-only, no minor version).

Confidence Score: 4/5

The UI and state-management changes are correct and safe to merge. The fast-polling logic in ipc_client.rs has a gap: a connection drop caught by the command arm doesn't revert the interval to fast cadence, so agent restarts that happen mid-command recover more slowly than intended.

The cmd arm of the select! loop sets client = None on handle() error but does not reset steady or the interval, leaving recovery at the 2 s steady cadence instead of 250 ms. The poll arm correctly does this reset, and the UI and loading_body refactoring are clean, but the inconsistency in the command path contradicts the stated reconnect-speed guarantee.

crates/openlogi-gui/src/ipc_client.rs — the cmd arm's error path is missing the steady/interval reset that the poll arm has

Important Files Changed

Filename Overview
crates/openlogi-gui/src/ipc_client.rs Adds fast-startup polling (250 ms) that promotes to steady (2 s) on first inventory_ready, and reverts to fast on connection loss — but the cmd arm doesn't reset steady/interval on error, so an agent restart triggered during a command keeps polling at 2 s instead of 250 ms.
crates/openlogi-gui/src/app.rs Extracts a shared loading_body helper, adds device_scanning_state using it, and properly separates the scanning-unknown path from the empty-state path — clean refactor with no issues.
crates/openlogi-agent-core/src/ipc.rs Documentation-only addition explaining the tarpc/bincode wire-format invariant (method order, append-only evolution, no minor version). No logic changes.

Sequence Diagram

sequenceDiagram
    participant GUI as GUI (app.rs)
    participant IPC as ipc_client.rs
    participant Agent

    Note over GUI: AppState.scanning = true
    GUI->>GUI: connecting_view (loading_body)

    loop Every 250 ms (STARTUP_POLL_PERIOD)
        IPC->>Agent: ensure() + inventory() + status()
        alt agent not up yet
            Agent-->>IPC: Err (socket not bound)
            IPC-->>IPC: Ok(None) — keep fast cadence
        else "agent up, inventory_ready = false"
            Agent-->>IPC: Ok(Some(false))
            IPC-->>GUI: "PollUpdate (scanning=true)"
            GUI->>GUI: device_scanning_state (loading_body)
        else "agent up, inventory_ready = true"
            Agent-->>IPC: Ok(Some(true))
            IPC-->>IPC: "steady=true, switch to poll_period (2s)"
            IPC-->>GUI: "PollUpdate (scanning=false)"
            alt devices present
                GUI->>GUI: device_gallery
            else no devices
                GUI->>GUI: device_empty_state
            end
        end
    end

    Note over IPC: On poll Err() (dropped connection)
    IPC-->>IPC: "steady=false, revert to 250 ms"
Loading

Comments Outside Diff (1)

  1. crates/openlogi-gui/src/ipc_client.rs, line 153-158 (link)

    P1 Fast-polling not restored on command-arm connection drop

    When handle() returns Err(()), client is set to None but steady is left true and the interval remains at poll_period (2 s). Because ensure() returns Ok(None) (not Err(())) while the agent is still coming back up, the poll arm's Err branch never fires to reset steady and the interval. The PR explicitly states "falls back to fast when the connection is lost so an agent restart (binary-update exec, crash) re-converges just as quickly", but that only holds for drops detected by the poll arm, not here. Recovery after a mid-command agent restart polls at 2 s cadence instead of 250 ms — up to 8× slower.

    Mirroring the poll arm's Err block here fixes it:

    if handle(&mut client, cmd).await.is_err() {
        client = None;
        if steady {
            steady = false;
            interval = tokio::time::interval(STARTUP_POLL_PERIOD);
        }
    }

    Fix in Codex Fix in Claude Code

Fix All in Codex Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(gui): don't double-poll on the stead..." | Re-trigger Greptile

Comment thread crates/openlogi-gui/src/ipc_client.rs
A fresh tokio interval ticks immediately, so switching to the steady period
fired a redundant poll back-to-back with the one that just confirmed
readiness; start the steady interval one period out via interval_at.
@AprilNEA AprilNEA merged commit 4c5d958 into master Jun 11, 2026
8 checks passed
@AprilNEA AprilNEA deleted the fix/scanning-state-flash branch June 11, 2026 06:31
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.
AprilNEA added a commit to davidbudnick/OpenLogi that referenced this pull request Jun 12, 2026
Resolve the IPC poll conflict against the startup-states rework and
adapt the diagnostics adapter to master:

- call store_agent_snapshot before the status snapshot moves into
  AgentLink::Ready; the inventory merge keeps the InventoryHealth gate
  (AprilNEA#213/AprilNEA#215/AprilNEA#220)
- read accessibility from the retained AgentStatus; the
  AppState.accessibility_granted field no longer exists
- map DeviceRoute::Unifying (AprilNEA#181) to a new
  ConnectionKind::UnifyingReceiver
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