@ggui-ai/iframe-runtime 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connection.d.ts +20 -0
- package/dist/connection.js +20 -0
- package/dist/iframe-runtime.js +93 -91
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/observability.d.ts +129 -21
- package/dist/observability.js +59 -0
- package/dist/react-renderer.d.ts +2 -5
- package/dist/react-renderer.js +20 -4
- package/dist/render-item.d.ts +2 -6
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +266 -65
- package/dist/status-dom.d.ts +9 -0
- package/dist/status-dom.js +11 -0
- package/dist/system-cards/UiFeedbackCard.d.ts +13 -8
- package/dist/system-cards/UiFeedbackCard.js +32 -41
- package/dist/ui-feedback-chrome.d.ts +30 -0
- package/dist/ui-feedback-chrome.js +22 -0
- package/dist/wire-config.js +9 -3
- package/package.json +15 -10
package/dist/runtime.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// FIRST import on purpose — declares zod jitless before any
|
|
28
28
|
// schema-defining dependency initializes (see zod-jitless.ts).
|
|
29
29
|
import './zod-jitless.js';
|
|
30
|
+
import { connectionStore } from './connection.js';
|
|
30
31
|
import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
31
32
|
import { effectiveThemeId, effectiveThemeMode, } from '@ggui-ai/protocol/integrations/theme-binding';
|
|
32
33
|
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
|
|
@@ -43,13 +44,13 @@ import { createChannelErrorHandler, createChannelPayloadHandler, createDataHandl
|
|
|
43
44
|
import { connectViaRegistry, } from './registry-subscribe.js';
|
|
44
45
|
import { buildBridgePolling, buildEventsPolling, createSequenceCursor, } from './events-polling.js';
|
|
45
46
|
import { unwrapCallToolResult } from './call-tool-unwrap.js';
|
|
46
|
-
import { ensureStatusDom, setStatus, setConnectedStatus, } from './status-dom.js';
|
|
47
|
+
import { ensureStatusDom, setStatus, setConnectedStatus, retireShellLoadingIndicator, } from './status-dom.js';
|
|
47
48
|
import { mergeReservedValidators, setActiveValidatorSet, } from './validation.js';
|
|
48
49
|
import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
|
|
49
50
|
import { mountRender, } from './render-item.js';
|
|
50
51
|
import { fromBootstrapFailure, } from './protocol-error.js';
|
|
51
52
|
import { postObservabilityToParent, } from './observability.js';
|
|
52
|
-
import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome } from './ui-feedback-chrome.js';
|
|
53
|
+
import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome, shouldMountUiFeedbackChrome, } from './ui-feedback-chrome.js';
|
|
53
54
|
import { hostCanReceiveMessages, hostCanRelayToolCalls, hostCapabilitiesCaptured, setHostCapabilities, } from './host-capabilities.js';
|
|
54
55
|
import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
|
|
55
56
|
import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
|
|
@@ -74,7 +75,15 @@ export function subscribeDrainAck(listener) {
|
|
|
74
75
|
drainAckListeners.delete(listener);
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The `drain_ack` ingress — fans a frame out to subscribers until one
|
|
80
|
+
* claims it.
|
|
81
|
+
*
|
|
82
|
+
* @internal — exported for unit tests: a late ack for an earlier
|
|
83
|
+
* delivered gesture is the path that retires the standing relay notice
|
|
84
|
+
* without any user click (ggui#670 Phase 3, truth-2).
|
|
85
|
+
*/
|
|
86
|
+
export function dispatchDrainAck(payload) {
|
|
78
87
|
for (const listener of drainAckListeners) {
|
|
79
88
|
try {
|
|
80
89
|
if (listener(payload) === true)
|
|
@@ -335,7 +344,7 @@ function postBootFailure(reason, message) {
|
|
|
335
344
|
}
|
|
336
345
|
}
|
|
337
346
|
export async function bootSequence(opts) {
|
|
338
|
-
const { doc, app, transport, notifyParent, renderer: rendererHooks, onProtocolError, onObserve, onLifecycle } = opts;
|
|
347
|
+
const { doc, app, transport, notifyParent, renderer: rendererHooks, onProtocolError, onObserve, onLifecycle, uiFeedback } = opts;
|
|
339
348
|
const connectFn = opts.connectFn ?? connectViaRegistry;
|
|
340
349
|
const toolResultTimeoutMs = opts.toolResultTimeoutMs ?? POSTMESSAGE_BOOT_TIMEOUT_MS;
|
|
341
350
|
// Emit typed {@link ProtocolError} for every bootstrap-failure site
|
|
@@ -415,6 +424,7 @@ export async function bootSequence(opts) {
|
|
|
415
424
|
const initResult = await connectApp(app, transport);
|
|
416
425
|
if (!initResult.ok) {
|
|
417
426
|
setStatus(refs, `ui/initialize failed: ${initResult.message}`, 'error');
|
|
427
|
+
retireShellLoadingIndicator(doc);
|
|
418
428
|
emitBootFailure('UI_INITIALIZE_FAILED', initResult.message);
|
|
419
429
|
return { ok: false, mountedRender };
|
|
420
430
|
}
|
|
@@ -511,6 +521,7 @@ export async function bootSequence(opts) {
|
|
|
511
521
|
if (!parsed.ok) {
|
|
512
522
|
const message = `slice-meta parse failed: ${parsed.reason}`;
|
|
513
523
|
setStatus(refs, message, 'error');
|
|
524
|
+
retireShellLoadingIndicator(doc);
|
|
514
525
|
emitBootFailure(parsed.reason, message);
|
|
515
526
|
return { ok: false, mountedRender };
|
|
516
527
|
}
|
|
@@ -547,10 +558,12 @@ export async function bootSequence(opts) {
|
|
|
547
558
|
...(onObserve !== undefined ? { onObserve } : {}),
|
|
548
559
|
})
|
|
549
560
|
: null;
|
|
550
|
-
// In-iframe UI-feedback affordance (ggui#244) —
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
561
|
+
// In-iframe UI-feedback affordance (ggui#244) — DEFAULT OFF since
|
|
562
|
+
// #653 (founder ruling): mounts only when the boot options opt in
|
|
563
|
+
// via `uiFeedback: true`. When opted in it is additionally gated
|
|
564
|
+
// inside the helper on `window.parent !== window` (top-level
|
|
565
|
+
// `/r/<shortCode>` tabs have no `ggui:observe` egress, so they get
|
|
566
|
+
// NO affordance) AND here on a LIVE SINK: an
|
|
554
567
|
// injected `onObserve` emitter, or a first-party embed host — the
|
|
555
568
|
// only parent that consumes the `ggui:observe` envelope
|
|
556
569
|
// (mcp-app-iframe-host answers `ui/initialize` with exactly this
|
|
@@ -560,9 +573,12 @@ export async function bootSequence(opts) {
|
|
|
560
573
|
// retest surfaced it as unstyled clutter that could never deliver
|
|
561
574
|
// feedback anywhere. Fire-and-forget: chrome never blocks or fails
|
|
562
575
|
// the boot.
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
576
|
+
const mountFeedback = shouldMountUiFeedbackChrome({
|
|
577
|
+
uiFeedback,
|
|
578
|
+
onObserveBound: onObserve !== undefined,
|
|
579
|
+
hostName: app.getHostVersion()?.name,
|
|
580
|
+
});
|
|
581
|
+
if (mountFeedback) {
|
|
566
582
|
void mountUiFeedbackChrome(doc, {
|
|
567
583
|
emit: onObserve ?? postObservabilityToParent,
|
|
568
584
|
sessionId: meta.sessionId,
|
|
@@ -630,6 +646,9 @@ export async function bootSequence(opts) {
|
|
|
630
646
|
if (codeReadyEmitted)
|
|
631
647
|
return;
|
|
632
648
|
codeReadyEmitted = true;
|
|
649
|
+
// First real paint — the served shell's working-state mark (#667)
|
|
650
|
+
// becomes a lie the moment content lands.
|
|
651
|
+
retireShellLoadingIndicator(doc);
|
|
633
652
|
onLifecycle?.(makeLifecycleEvent('code-ready', { sessionId: meta.sessionId }));
|
|
634
653
|
};
|
|
635
654
|
// Mode discriminators. The mount surface is DECOUPLED from the live
|
|
@@ -659,6 +678,10 @@ export async function bootSequence(opts) {
|
|
|
659
678
|
// Fresh boot ⇒ not superseded (matters only for test re-runs that
|
|
660
679
|
// reuse the module; a real iframe boots once).
|
|
661
680
|
mountSuperseded = false;
|
|
681
|
+
// Fresh boot ⇒ the relay latch and the connection store start
|
|
682
|
+
// aligned (ggui#670): a standing latch from a prior mount in a
|
|
683
|
+
// re-booted document must not outlive the store it was written to.
|
|
684
|
+
resetRelayLatchForBoot();
|
|
662
685
|
telemetry?.record('boot.path', JSON.stringify({
|
|
663
686
|
hasStaticContent,
|
|
664
687
|
hasLiveTrio,
|
|
@@ -721,6 +744,7 @@ export async function bootSequence(opts) {
|
|
|
721
744
|
if (!hasLiveTrio && mountedRender === null) {
|
|
722
745
|
const message = 'bootstrap carries neither static content (codeUrl/codeB64/kind) nor a live trio (wsUrl/wsToken)';
|
|
723
746
|
setStatus(refs, message, 'error');
|
|
747
|
+
retireShellLoadingIndicator(doc);
|
|
724
748
|
emitBootFailure('MISSING_META_GGUI_BOOTSTRAP', message);
|
|
725
749
|
return { ok: false, mountedRender };
|
|
726
750
|
}
|
|
@@ -1595,13 +1619,65 @@ async function callServerToolSpec(toolName, args) {
|
|
|
1595
1619
|
return { jsonrpc: '2.0', result: result };
|
|
1596
1620
|
}
|
|
1597
1621
|
catch (err) {
|
|
1622
|
+
// Preserve the JSON-RPC error code when the SDK throw carries one
|
|
1623
|
+
// (McpError does) — ggui#599 cycle-2: without it a genuine helper
|
|
1624
|
+
// refusal (-32601 / -32000) is indistinguishable from a socket
|
|
1625
|
+
// drop at both latch call sites, and the confirmed-refusal
|
|
1626
|
+
// classification below this file cannot exist. Code-less throws
|
|
1627
|
+
// stay code-less: transport-shaped, never a confirmed refusal.
|
|
1628
|
+
const code = err.code;
|
|
1598
1629
|
return {
|
|
1599
1630
|
error: {
|
|
1631
|
+
...(typeof code === 'number' ? { code } : {}),
|
|
1600
1632
|
message: err instanceof Error ? err.message : String(err),
|
|
1601
1633
|
},
|
|
1602
1634
|
};
|
|
1603
1635
|
}
|
|
1604
1636
|
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Declared relay-refusal registry (ggui#599 cycle-2, finding 34): the
|
|
1639
|
+
* JSON-RPC error codes a HELPER mints to refuse the relay leg itself —
|
|
1640
|
+
* proof the envelope never round-tripped to the server. Server-side
|
|
1641
|
+
* tool failures come back as well-formed `CallToolResult`s (often
|
|
1642
|
+
* `{ok:false}`), never as JSON-RPC errors on this leg, so a registry
|
|
1643
|
+
* code here is helper-minted by construction:
|
|
1644
|
+
*
|
|
1645
|
+
* - `-32601` — method not supported (the spec's in-band refusal; the
|
|
1646
|
+
* host-helper conformance catalog's H3 shape). The ONLY registry
|
|
1647
|
+
* entry, deliberately.
|
|
1648
|
+
*
|
|
1649
|
+
* `-32000` is EXCLUDED by ruling (ggui#599 cycle-2): it collides with
|
|
1650
|
+
* the MCP SDK's `ErrorCode.ConnectionClosed`, so through the App-SDK
|
|
1651
|
+
* catch a helper-minted -32000 refusal and an SDK-local transport loss
|
|
1652
|
+
* are indistinguishable — and a transport loss must NEVER classify
|
|
1653
|
+
* (misbranding a dropped connection as incapability is the
|
|
1654
|
+
* misclassification asymmetry the cycle killed the mount probe over).
|
|
1655
|
+
* The one live -32000 minter (the RN dispatcher's `no-tool-handler`)
|
|
1656
|
+
* was fixed at source to -32601 in the same slice.
|
|
1657
|
+
*
|
|
1658
|
+
* Unknown codes NEVER confirm — they may be proxy- or server-minted
|
|
1659
|
+
* (e.g. `-32603` internal error), and classifying them would misbrand
|
|
1660
|
+
* capable mounts. They stay on the per-gesture transient path.
|
|
1661
|
+
*/
|
|
1662
|
+
const CONFIRMED_RELAY_REFUSAL_CODES = new Set([-32601]);
|
|
1663
|
+
/**
|
|
1664
|
+
* A CONFIRMED relay refusal: the response is an error envelope whose
|
|
1665
|
+
* code is in the declared registry. Strictly stronger evidence than
|
|
1666
|
+
* {@link isRelayShapedFailure} (every confirmed refusal is
|
|
1667
|
+
* relay-shaped; a null/timeout or code-less error never confirms).
|
|
1668
|
+
* Confirmed refusal outranks a positive `serverTools` advertisement at
|
|
1669
|
+
* the latch — classification keys on confirmed attempt outcomes, never
|
|
1670
|
+
* advertisement (#440's doctrine applied symmetrically; the
|
|
1671
|
+
* advertises-but-refuses host is H2's worst dead-tap shape and was
|
|
1672
|
+
* unlatchable before this predicate existed).
|
|
1673
|
+
*/
|
|
1674
|
+
function isConfirmedRelayRefusal(resp) {
|
|
1675
|
+
return (resp !== null &&
|
|
1676
|
+
resp.error !== undefined &&
|
|
1677
|
+
resp.error !== null &&
|
|
1678
|
+
typeof resp.error.code === 'number' &&
|
|
1679
|
+
CONFIRMED_RELAY_REFUSAL_CODES.has(resp.error.code));
|
|
1680
|
+
}
|
|
1605
1681
|
/**
|
|
1606
1682
|
* Fire a single canonical action envelope (`tools/call
|
|
1607
1683
|
* ggui_runtime_submit_action`) carrying the typed `{kind, payload, …}` shape
|
|
@@ -1919,13 +1995,12 @@ function hideToast(el) {
|
|
|
1919
1995
|
el.setAttribute('aria-hidden', 'true');
|
|
1920
1996
|
clearToastAnnouncement();
|
|
1921
1997
|
}
|
|
1998
|
+
/** The single action-toast element, reused for every message. */
|
|
1999
|
+
const ACTION_TOAST_ID = '__ggui-action-toast__';
|
|
1922
2000
|
function showActionToast(text, kind, onDismiss) {
|
|
1923
2001
|
if (typeof document === 'undefined')
|
|
1924
2002
|
return;
|
|
1925
|
-
const
|
|
1926
|
-
if (w.__GGUI_TOAST_DISABLED__)
|
|
1927
|
-
return;
|
|
1928
|
-
const id = '__ggui-action-toast__';
|
|
2003
|
+
const id = ACTION_TOAST_ID;
|
|
1929
2004
|
let el = document.getElementById(id);
|
|
1930
2005
|
if (!el) {
|
|
1931
2006
|
el = document.createElement('div');
|
|
@@ -1961,6 +2036,9 @@ function showActionToast(text, kind, onDismiss) {
|
|
|
1961
2036
|
: 'rgba(178,54,54,.94)';
|
|
1962
2037
|
el.style.background = bg;
|
|
1963
2038
|
el.textContent = text;
|
|
2039
|
+
// Any message rendered here replaces whatever stood before — a
|
|
2040
|
+
// standing relay notice included; the mark goes with it.
|
|
2041
|
+
el.removeAttribute(RELAY_NOTICE_ATTR);
|
|
1964
2042
|
el.style.opacity = '1';
|
|
1965
2043
|
el.style.transform = 'translateX(-50%) translateY(0)';
|
|
1966
2044
|
// Reset to the non-interactive posture every time, so a toast that
|
|
@@ -2191,6 +2269,131 @@ function emitUserActionDoorbell(args) {
|
|
|
2191
2269
|
* nothing else about the latch is observable to the host.
|
|
2192
2270
|
*/
|
|
2193
2271
|
let relayIncapabilityAnnounced = false;
|
|
2272
|
+
/**
|
|
2273
|
+
* The current dead zone's origin and tally (ggui#670 Phase 3 — the
|
|
2274
|
+
* instrument's state): when the latch set, what set it, which render
|
|
2275
|
+
* it belongs to, and how many gestures have been attempted inside it.
|
|
2276
|
+
* Written only by {@link transitionRelayLatch} and
|
|
2277
|
+
* {@link emitRelayDeadTap}; read by nothing else.
|
|
2278
|
+
*/
|
|
2279
|
+
let relayLatchedAt;
|
|
2280
|
+
let relayDeadTaps = 0;
|
|
2281
|
+
let relayLatchTrigger;
|
|
2282
|
+
let relayLatchScope = {};
|
|
2283
|
+
function transitionRelayLatch(next, trigger, scope = {}) {
|
|
2284
|
+
relayIncapabilityAnnounced = next;
|
|
2285
|
+
connectionStore.set(!next);
|
|
2286
|
+
if (next && trigger !== undefined) {
|
|
2287
|
+
relayLatchedAt = Date.now();
|
|
2288
|
+
relayDeadTaps = 0;
|
|
2289
|
+
relayLatchTrigger = trigger;
|
|
2290
|
+
relayLatchScope = scope;
|
|
2291
|
+
postObservabilityToParent({
|
|
2292
|
+
kind: 'relay-incapability',
|
|
2293
|
+
state: 'latched',
|
|
2294
|
+
trigger,
|
|
2295
|
+
...scope,
|
|
2296
|
+
});
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
postObservabilityToParent({
|
|
2300
|
+
kind: 'relay-incapability',
|
|
2301
|
+
state: 'cleared',
|
|
2302
|
+
latchedForMs: relayLatchedAt === undefined ? 0 : Date.now() - relayLatchedAt,
|
|
2303
|
+
deadTaps: relayDeadTaps,
|
|
2304
|
+
...relayLatchScope,
|
|
2305
|
+
});
|
|
2306
|
+
relayLatchedAt = undefined;
|
|
2307
|
+
relayDeadTaps = 0;
|
|
2308
|
+
relayLatchTrigger = undefined;
|
|
2309
|
+
relayLatchScope = {};
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* One {@link RelayDeadTapEvent} per gesture attempted inside a standing
|
|
2313
|
+
* dead zone that came back UNDELIVERED (ggui#670 Phase 3). Called from
|
|
2314
|
+
* the dispatch outcome branch ONLY — the channel router's fail-fast
|
|
2315
|
+
* never dispatches, so poll ticks are never counted; off-latch it is a
|
|
2316
|
+
* no-op by construction. Counted at the outcome, not the tap, so the
|
|
2317
|
+
* gesture that heals the zone is never a dead tap.
|
|
2318
|
+
*/
|
|
2319
|
+
function emitRelayDeadTap(intent, scope) {
|
|
2320
|
+
if (!relayIncapabilityAnnounced ||
|
|
2321
|
+
relayLatchedAt === undefined ||
|
|
2322
|
+
relayLatchTrigger === undefined) {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
relayDeadTaps += 1;
|
|
2326
|
+
postObservabilityToParent({
|
|
2327
|
+
kind: 'relay-dead-tap',
|
|
2328
|
+
intent,
|
|
2329
|
+
latchAgeMs: Date.now() - relayLatchedAt,
|
|
2330
|
+
ordinal: relayDeadTaps,
|
|
2331
|
+
trigger: relayLatchTrigger,
|
|
2332
|
+
...scope,
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Marks the toast element as the STANDING relay notice. The dead-zone
|
|
2337
|
+
* cue arms on this element's absence or invisibility — however it went
|
|
2338
|
+
* away (the user's click or a late `drain_ack` retiring it). `showActionToast`
|
|
2339
|
+
* drops the mark whenever it renders any other message, so a fallback
|
|
2340
|
+
* cue or a later pending toast never passes for the explanation.
|
|
2341
|
+
*/
|
|
2342
|
+
const RELAY_NOTICE_ATTR = 'data-ggui-relay-notice';
|
|
2343
|
+
/**
|
|
2344
|
+
* Tag the just-shown notice and place it BEFORE the render root in
|
|
2345
|
+
* document order, so a keyboard or virtual-cursor user meets the
|
|
2346
|
+
* explanation before the dead controls it explains (ggui#670 Phase 3,
|
|
2347
|
+
* a11y fold). Reading order only — the element's fixed-position paint
|
|
2348
|
+
* is unchanged.
|
|
2349
|
+
*/
|
|
2350
|
+
function markRelayNotice() {
|
|
2351
|
+
if (typeof document === 'undefined')
|
|
2352
|
+
return;
|
|
2353
|
+
const el = document.getElementById(ACTION_TOAST_ID);
|
|
2354
|
+
if (el === null)
|
|
2355
|
+
return; // no document surface to mark (pre-boot); the cue covers every attempt
|
|
2356
|
+
el.setAttribute(RELAY_NOTICE_ATTR, '');
|
|
2357
|
+
const root = document.querySelector('[data-ggui-session-root]');
|
|
2358
|
+
if (root !== null &&
|
|
2359
|
+
root.parentNode !== null &&
|
|
2360
|
+
(root.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) {
|
|
2361
|
+
root.parentNode.insertBefore(el, root);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
/** Whether the standing relay notice is on screen right now. */
|
|
2365
|
+
function isRelayNoticeVisible() {
|
|
2366
|
+
if (typeof document === 'undefined')
|
|
2367
|
+
return false;
|
|
2368
|
+
const el = document.getElementById(ACTION_TOAST_ID);
|
|
2369
|
+
return (el !== null &&
|
|
2370
|
+
el.hasAttribute(RELAY_NOTICE_ATTR) &&
|
|
2371
|
+
el.style.opacity !== '0' &&
|
|
2372
|
+
el.getAttribute('aria-hidden') !== 'true');
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Boot-time reset of the relay latch (ggui#670 Phase 3). A latch
|
|
2376
|
+
* standing from a prior mount in a re-booted document is closed through
|
|
2377
|
+
* the ONE writer — the host gets its `cleared` edge with the zone's
|
|
2378
|
+
* summary — and the prior mount's standing notice is removed rather
|
|
2379
|
+
* than left describing a session that no longer exists.
|
|
2380
|
+
*
|
|
2381
|
+
* @internal — exported for unit tests.
|
|
2382
|
+
*/
|
|
2383
|
+
export function resetRelayLatchForBoot() {
|
|
2384
|
+
if (typeof document !== 'undefined') {
|
|
2385
|
+
const el = document.getElementById(ACTION_TOAST_ID);
|
|
2386
|
+
if (el !== null && el.hasAttribute(RELAY_NOTICE_ATTR))
|
|
2387
|
+
el.remove();
|
|
2388
|
+
}
|
|
2389
|
+
resetRelayCueThrottles();
|
|
2390
|
+
if (relayIncapabilityAnnounced) {
|
|
2391
|
+
transitionRelayLatch(false);
|
|
2392
|
+
}
|
|
2393
|
+
else {
|
|
2394
|
+
connectionStore.set(true);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2194
2397
|
/**
|
|
2195
2398
|
* Module-level handle to the CURRENT boot's telemetry sink so
|
|
2196
2399
|
* module-scope paths (the doorbell emitter below) can report without
|
|
@@ -2208,24 +2411,6 @@ let currentTelemetrySink = null;
|
|
|
2208
2411
|
* `currentTelemetrySink`.
|
|
2209
2412
|
*/
|
|
2210
2413
|
let mountSuperseded = false;
|
|
2211
|
-
/**
|
|
2212
|
-
* Whether the user has manually dismissed the CURRENTLY-STANDING relay
|
|
2213
|
-
* notice (ggui#442).
|
|
2214
|
-
*
|
|
2215
|
-
* "Dismissed the one explanation" is not "wants zero feedback forever",
|
|
2216
|
-
* but it IS consent that the explanation itself has been read. So the
|
|
2217
|
-
* dead zone this opens is filled with a per-gesture CUE, never with the
|
|
2218
|
-
* notice again.
|
|
2219
|
-
*
|
|
2220
|
-
* Scoped to the notice, not to toasts in general: the doorbell's
|
|
2221
|
-
* `action_required` toast is a different message with a different
|
|
2222
|
-
* dismissal meaning, so the flag is set from the relay notice's own
|
|
2223
|
-
* `onDismiss` callback rather than from `showActionToast` at large.
|
|
2224
|
-
*
|
|
2225
|
-
* Reset on BOTH latch edges — a re-latch shows a fresh notice nobody
|
|
2226
|
-
* has dismissed yet, and a self-heal must leave no residue behind.
|
|
2227
|
-
*/
|
|
2228
|
-
let relayNoticeDismissed = false;
|
|
2229
2414
|
/**
|
|
2230
2415
|
* Timestamp of the last fallback cue toast, for the throttle below.
|
|
2231
2416
|
* Reset alongside the latch so a new dead zone never inherits an old
|
|
@@ -2301,7 +2486,11 @@ function resetRelayCueThrottles() {
|
|
|
2301
2486
|
/** @internal — exported for unit tests to reset module state. */
|
|
2302
2487
|
export function __resetRelayNoticeForTest() {
|
|
2303
2488
|
relayIncapabilityAnnounced = false;
|
|
2304
|
-
|
|
2489
|
+
connectionStore.set(true);
|
|
2490
|
+
relayLatchedAt = undefined;
|
|
2491
|
+
relayDeadTaps = 0;
|
|
2492
|
+
relayLatchTrigger = undefined;
|
|
2493
|
+
relayLatchScope = {};
|
|
2305
2494
|
resetRelayCueThrottles();
|
|
2306
2495
|
}
|
|
2307
2496
|
/**
|
|
@@ -2361,10 +2550,8 @@ function resolveRelayCueTarget() {
|
|
|
2361
2550
|
* be a lie about someone else's element. The honest counterpart is a
|
|
2362
2551
|
* message in the announcer's own region, saying what the pulse means.
|
|
2363
2552
|
*
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
2366
|
-
* way. Announcing on the same condition as the visual keeps the two
|
|
2367
|
-
* cues telling the same story.
|
|
2553
|
+
* Announcing on the same condition as the visual keeps the two cues
|
|
2554
|
+
* telling the same story.
|
|
2368
2555
|
*
|
|
2369
2556
|
* Throttled on its own clock, unlike the pulse — but only against
|
|
2370
2557
|
* IDENTICAL repeats. A flash repeating on every gesture costs a sighted
|
|
@@ -2420,8 +2607,10 @@ function retractRelayCueAnnouncement() {
|
|
|
2420
2607
|
}
|
|
2421
2608
|
/**
|
|
2422
2609
|
* The dead-zone cue (ggui#442): the smallest honest "that did nothing"
|
|
2423
|
-
* signal, for a gesture on a host already known
|
|
2424
|
-
*
|
|
2610
|
+
* signal, for a gesture on a host already known to be relay-incapable
|
|
2611
|
+
* while the standing explanation is NOT on screen — dismissed by the
|
|
2612
|
+
* user or retired by a late `drain_ack` (ggui#670 Phase 3: the cue arms
|
|
2613
|
+
* on the notice's absence, never on how it went away).
|
|
2425
2614
|
*
|
|
2426
2615
|
* Component-agnostic by construction. It pulses whatever the user
|
|
2427
2616
|
* focused without knowing what that is, and falls back to the toast
|
|
@@ -2429,7 +2618,7 @@ function retractRelayCueAnnouncement() {
|
|
|
2429
2618
|
* (ggui#447) — the pulse via {@link announceRelayCue}, the fallback via
|
|
2430
2619
|
* the toast primitive's own announcement.
|
|
2431
2620
|
*/
|
|
2432
|
-
function
|
|
2621
|
+
function showDeadZoneCue(intent) {
|
|
2433
2622
|
if (typeof document === 'undefined')
|
|
2434
2623
|
return;
|
|
2435
2624
|
const target = resolveRelayCueTarget();
|
|
@@ -2538,16 +2727,20 @@ export function dispatchSubmitAction(args) {
|
|
|
2538
2727
|
// that nothing downstream ever restores — the opposite of "left
|
|
2539
2728
|
// standing" (see the latch declaration + terminal branch below).
|
|
2540
2729
|
//
|
|
2541
|
-
//
|
|
2542
|
-
//
|
|
2543
|
-
//
|
|
2544
|
-
//
|
|
2545
|
-
//
|
|
2730
|
+
// While the latch stands the gesture still dispatches (attempt-always,
|
|
2731
|
+
// #443 — the attempt is the self-heal sensor); its outcome is counted
|
|
2732
|
+
// for the host where it lands (ggui#670 Phase 3, the terminal branch
|
|
2733
|
+
// below). When the standing explanation is not on screen — dismissed,
|
|
2734
|
+
// or retired by a late `drain_ack` — skipping would leave the gesture
|
|
2735
|
+
// with no feedback at all: the dead zone ggui#442 names. The cue fills it
|
|
2736
|
+
// here, at dispatch time, because the element it pulses is the one
|
|
2737
|
+
// the user has focused NOW; by the time the relay response settles,
|
|
2738
|
+
// focus may have moved on.
|
|
2546
2739
|
if (!relayIncapabilityAnnounced) {
|
|
2547
2740
|
showActionToast(`→ ${intent}${dataPart}`, 'pending');
|
|
2548
2741
|
}
|
|
2549
|
-
else if (
|
|
2550
|
-
|
|
2742
|
+
else if (!isRelayNoticeVisible()) {
|
|
2743
|
+
showDeadZoneCue(intent);
|
|
2551
2744
|
}
|
|
2552
2745
|
// (2) Try submit_action via host relay. Spec-compliant hosts
|
|
2553
2746
|
// forward the tools/call to the MCP server's submit_action handler;
|
|
@@ -2611,16 +2804,11 @@ export function dispatchSubmitAction(args) {
|
|
|
2611
2804
|
if (relayIncapabilityAnnounced &&
|
|
2612
2805
|
resp !== null &&
|
|
2613
2806
|
!isRelayShapedFailure(resp)) {
|
|
2614
|
-
relayIncapabilityAnnounced = false;
|
|
2615
2807
|
// Self-heal leaves NO cue residue (ggui#442): the dead zone is
|
|
2616
2808
|
// gone, so the state that armed the cue goes with it. A later
|
|
2617
2809
|
// re-latch starts from a clean slate.
|
|
2618
|
-
relayNoticeDismissed = false;
|
|
2619
2810
|
resetRelayCueThrottles();
|
|
2620
|
-
|
|
2621
|
-
kind: 'relay-incapability',
|
|
2622
|
-
state: 'cleared',
|
|
2623
|
-
});
|
|
2811
|
+
transitionRelayLatch(false);
|
|
2624
2812
|
// The (1.5) pending toast above was SKIPPED for this gesture —
|
|
2625
2813
|
// the latch was still standing at dispatch time — so a bare
|
|
2626
2814
|
// clear leaves the now-false "cannot relay" notice standing with
|
|
@@ -2697,19 +2885,26 @@ export function dispatchSubmitAction(args) {
|
|
|
2697
2885
|
// standing latch at the response-arrival guard above, and
|
|
2698
2886
|
// keeps routing to the ordinary per-gesture transient toast
|
|
2699
2887
|
// below instead.
|
|
2888
|
+
// Two evidence classes latch (ggui#599 cycle-2): the original
|
|
2889
|
+
// advert-silent leg (nothing advertised AND the attempt failed
|
|
2890
|
+
// relay-shaped), and the CONFIRMED-REFUSAL leg — a declared
|
|
2891
|
+
// helper-minted refusal code, which outranks any positive
|
|
2892
|
+
// advertisement (the advertises-but-refuses host previously got a
|
|
2893
|
+
// fresh transient toast per tap, forever). Timeouts and unknown
|
|
2894
|
+
// codes never reach the second leg; the un-classify edge for BOTH
|
|
2895
|
+
// is the same response-arrival clear guard above (attempt-always
|
|
2896
|
+
// preserved — nothing here suppresses dispatch, per the #443
|
|
2897
|
+
// doctrine: the attempt is the self-heal sensor).
|
|
2898
|
+
const confirmedRefusal = isConfirmedRelayRefusal(resp);
|
|
2700
2899
|
if (hostCapabilitiesCaptured() &&
|
|
2701
|
-
!hostCanRelayToolCalls() &&
|
|
2702
|
-
|
|
2900
|
+
((!hostCanRelayToolCalls() && isRelayShapedFailure(resp)) ||
|
|
2901
|
+
confirmedRefusal)) {
|
|
2703
2902
|
if (!relayIncapabilityAnnounced) {
|
|
2704
|
-
relayIncapabilityAnnounced = true;
|
|
2705
2903
|
// Transition edge — the paired 'cleared' edge lives at the
|
|
2706
2904
|
// response-arrival guard above. NEVER emit off-edge (e.g. per
|
|
2707
2905
|
// channel poll tick): the router ticks on an interval and
|
|
2708
2906
|
// would spam; the two edges carry the full information.
|
|
2709
|
-
|
|
2710
|
-
kind: 'relay-incapability',
|
|
2711
|
-
state: 'latched',
|
|
2712
|
-
});
|
|
2907
|
+
transitionRelayLatch(true, confirmedRefusal ? 'confirmed-refusal' : 'advert-silent', { sessionId, appId });
|
|
2713
2908
|
// Establish the flag's invariant where the notice it describes
|
|
2714
2909
|
// is created: a freshly-shown notice is undismissed, and its
|
|
2715
2910
|
// dead zone starts with an unspent throttle. Today every
|
|
@@ -2717,11 +2912,17 @@ export function dispatchSubmitAction(args) {
|
|
|
2717
2912
|
// resets both), so this is the same value arriving by a second
|
|
2718
2913
|
// route — kept because the state belongs to THIS notice, and a
|
|
2719
2914
|
// future second latch-set path shouldn't have to know that.
|
|
2720
|
-
relayNoticeDismissed = false;
|
|
2721
2915
|
resetRelayCueThrottles();
|
|
2722
|
-
showActionToast('This host cannot relay actions to the agent — interactive controls will not work here.', 'action_required'
|
|
2723
|
-
|
|
2724
|
-
|
|
2916
|
+
showActionToast('This host cannot relay actions to the agent — interactive controls will not work here.', 'action_required');
|
|
2917
|
+
markRelayNotice();
|
|
2918
|
+
}
|
|
2919
|
+
else {
|
|
2920
|
+
// A gesture attempted inside a standing dead zone came back
|
|
2921
|
+
// undelivered — a dead tap, counted for the host (ggui#670
|
|
2922
|
+
// Phase 3). The latching gesture above is the 'latched' edge,
|
|
2923
|
+
// and a gesture that heals the zone is the 'cleared' edge —
|
|
2924
|
+
// neither is a dead tap.
|
|
2925
|
+
emitRelayDeadTap(intent, { sessionId, appId });
|
|
2725
2926
|
}
|
|
2726
2927
|
return;
|
|
2727
2928
|
}
|
package/dist/status-dom.d.ts
CHANGED
|
@@ -75,3 +75,12 @@ export declare function setStatus(refs: StatusRefs, text: string, state: StatusK
|
|
|
75
75
|
* intercepting `console.log`) get a stable string on every render.
|
|
76
76
|
*/
|
|
77
77
|
export declare function setConnectedStatus(refs: StatusRefs): void;
|
|
78
|
+
/**
|
|
79
|
+
* Retire the served shell's pre-render working-state mark (#667 —
|
|
80
|
+
* `[data-ggui-shell-loading]`, the ggui glyph treatment or an
|
|
81
|
+
* operator's court block). Called at `code-ready` (the first real
|
|
82
|
+
* paint makes a loading mark a lie) and on terminal boot failure (the
|
|
83
|
+
* failure card must not sit under an "everything is fine" animation).
|
|
84
|
+
* Idempotent; a shell without the mark is a no-op.
|
|
85
|
+
*/
|
|
86
|
+
export declare function retireShellLoadingIndicator(doc: Document): void;
|
package/dist/status-dom.js
CHANGED
|
@@ -66,3 +66,14 @@ export function setStatus(refs, text, state) {
|
|
|
66
66
|
export function setConnectedStatus(refs) {
|
|
67
67
|
setStatus(refs, 'Connected.', 'connected');
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Retire the served shell's pre-render working-state mark (#667 —
|
|
71
|
+
* `[data-ggui-shell-loading]`, the ggui glyph treatment or an
|
|
72
|
+
* operator's court block). Called at `code-ready` (the first real
|
|
73
|
+
* paint makes a loading mark a lie) and on terminal boot failure (the
|
|
74
|
+
* failure card must not sit under an "everything is fine" animation).
|
|
75
|
+
* Idempotent; a shell without the mark is a no-op.
|
|
76
|
+
*/
|
|
77
|
+
export function retireShellLoadingIndicator(doc) {
|
|
78
|
+
doc.querySelector('[data-ggui-shell-loading]')?.remove();
|
|
79
|
+
}
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
* ("did this generated UI work for you?").
|
|
4
4
|
*
|
|
5
5
|
* The in-iframe twin of the host-chrome `UiFeedback` component in
|
|
6
|
-
* `@ggui-ai/mcp-apps-react`: same
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* `@ggui-ai/mcp-apps-react`: same two verdicts — thumbs up / thumbs
|
|
7
|
+
* down (#653) — same acknowledgement after submit, same dismiss
|
|
8
|
+
* control, same payload semantics (context stamps present exactly when
|
|
9
|
+
* known). Where the host-chrome twin hands a payload to an
|
|
10
|
+
* `onUiFeedback` callback, this card builds a {@link UiFeedbackEvent}
|
|
11
|
+
* and hands it to the injected {@link ObservabilityEmitter} —
|
|
12
|
+
* production binds the `ggui:observe` postMessage-to-parent default,
|
|
13
|
+
* tests record.
|
|
14
|
+
*
|
|
15
|
+
* The verdict buttons are icon-only: monochrome stroked SVGs drawn in
|
|
16
|
+
* `currentColor`, so the icon ink follows the button's `--ggui-*`
|
|
17
|
+
* color token in both modes (the #653 theme-applicability
|
|
18
|
+
* requirement). The verdict's name lives in the `aria-label`.
|
|
14
19
|
*
|
|
15
20
|
* This is CHROME the runtime mounts adjacent to the session root
|
|
16
21
|
* (see `ui-feedback-chrome.ts`), NOT a system card: it has no
|