fix(apps): bind app elicitations to the connection that made them

Two of the three findings; the third is answered in the PR thread.

**A replaced client could still enqueue.** The one-shot sweep only rejected what
was already queued, but the outgoing `InspectorClient` disconnects
asynchronously and can enqueue during its own teardown — and that late entry
would be rendered by a factory bound to the REPLACEMENT client, reading its
resource and answering through a different server. Requests now belong to a
session: each constructed client gets one, closing it rejects that session's
entries AND refuses anything it queues afterwards, and another session's
entries are untouched.

**The advertisement no longer has to guess.** Whether a sandbox exists is only
known once `/api/config` resolves, and the answer is baked into the client at
construction — guessing "available" over-claims, guessing "unavailable" strands
the whole session on the native form despite having a sandbox. The connect path
now awaits that fetch (already in flight since mount, so no human ever waits;
it only orders a deep-link auto-connect racing the same page load), after which
an absent `sandboxUrl` means confirmed-absent.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-22 22:55:43 -04:00
commit 0f3ee265fc
3 changed files with 178 additions and 46 deletions

View file

@ -23,6 +23,7 @@ function entry(
): AppElicitationEntry {
return {
requestId,
sessionId: 0,
resourceUri,
params,
signal: new AbortController().signal,

View file

@ -62,6 +62,7 @@ function makeEntry(
): AppElicitationEntry {
return {
requestId,
sessionId: 0,
resourceUri,
params,
signal: new AbortController().signal,

View file

@ -236,6 +236,9 @@ function AppElicitationFrame({
return (
<ElicitationModal
// `opened` stays at the call site: it is REQUIRED by `ModalRootProps`,
// and `.withProps()` supplies defaults without relaxing a required prop,
// so baking it in leaves every call site failing TS2741.
opened
onClose={dismiss}
trapFocus={isTop}