Closed general

Mirrored approvals never render: a Rust-emitted permission-request does not reach PermissionPrompt, though the same event emitted from the webview does

John Lauer · 1mo ago ·closed by John Lauer

A Rust-emitted permission-request never reaches PermissionPrompt, so mirrored approvals render nothing

Chasing "ab prompted and nothing appeared in Hydrogen" (adom/adom-bridge#137) I found two independent faults. The first is fixed in 1.0.197. This is the second, and it is the one that actually swallows the prompt.

The evidence, in the order it rules things out

On ah 1.0.197, laptop and winvm both:

  1. POST /permission/request answers displayed=true role=mirror, so the backend is not suppressing (the stale-flag bug is fixed and this is not it).
  2. The event does reach the webview. I registered my own listener from the page and it received the payload intact: plugin:event|listen {event:'permission-request', target:{kind:'Any'}} → {"event":"permission-request","payload":{"id":"test-evt-1","command":"echo probe",...},"id":51}
  3. Nothing renders: document.querySelector('.perm-backdrop') is null, no console error, no unhandledrejection, no window.onerror, across a reload.
  4. The component is mounted and its listener is alive. Emitting the identical event from inside the webview renders it perfectly: I.invoke('plugin:event|emit', {event:'permission-request', payload:{...}}) → {"shown":true,"badge":"EXAMPLE: nothing will run","buttons":["Once","1 hour","24 hours","Bypass"]}
  5. Rust-emitted events in general are fine. POST /wake/simulate (Rust app.emit("wake-status", …)) is received by the frontend and drives the post-wake reconcile: [resume] wake:simulated: workspace answering after 1s, reconciling views

So: same window, same event name, same listener. Emitted from the webview it renders; emitted from Rust it does not, while a raw listener with target: Any in that same window does receive the Rust emit.

Where I would look

PermissionPrompt.svelte subscribes with listen('permission-request', …) from @tauri-apps/api/event, which carries a default target. The mirror handler emits with Emitter::emit(&*app_handle, …) on the AppHandle held behind the control server's Arc<Mutex<…>>, taken with app.lock().await immediately before the emit. wake_heal emits on a plain AppHandle with no lock and works. The difference is either the event target the JS default subscribes to versus what an AppHandle emit delivers, or something about emitting while that mutex is held.

Why it matters

Every mechanism built around this prompt is intact and unreachable: the queue for simultaneous requests, the top-layer eviction that closes native <dialog> modals so an approval cannot hide behind Settings, the permission-dismiss retraction and the 3-second self-heal poll against ab's /permission/state. None of it runs, and both ends report success: ab is told the mirror was handled and ah logs it as displayed. A user simply never sees the prompt, which is what was reported on adom/adom-bridge#137.

This also blocks verifying the type-driven choice sets added in 1.0.196, since no mirrored prompt can be made to appear.

Seen on ah 1.0.197 (insiders) on AdomLapper and winvm, ab 2.1.18.

1 Reply

John Lauer · 1mo ago

Closing: this was my test artifact, not a product bug. The Rust-emitted event reaches PermissionPrompt and renders.

What actually happened. ah's mirror handler spawns a self-heal poll: every 3 seconds it asks ab /permission/state and, if the request id is no longer pending there, emits permission-dismiss and the dialog closes (lib.rs ~20470). Every request in my elimination chain was synthetic, with an id ab had never seen, so ab reported it not pending on the first poll and the dialog was closed at t=3s. Every DOM read I took was at t>=3s. The preview payload rendered because it is emitted from the webview and never goes through the mirror handler, so no poll was spawned for it. That combination produced a perfectly consistent, perfectly wrong picture.

The proof, ah 1.0.197 on AdomLapper, laptop idle, one synthetic shell mirror:

posted role=mirror at 97ms
t=427ms  {"shown":true,"buttons":["Once","1 hour","24 hours","Bypass"]}
t=1654ms {"shown":true,"buttons":["Once","1 hour","24 hours","Bypass"]}
t=4329ms {"shown":false,"buttons":[]}

What stands from this investigation. The stale suppress-flag bug was real and is fixed (1.0.197, flag re-asserted from the setting at boot). And the self-heal is doing exactly what it should: a request ab does not consider pending must not sit on screen. The lesson is for testing, not the code: a synthetic mirror is only visible for 3 seconds, so any probe of this dialog must read the DOM inside that window or use a real ab request.

Log in to reply.