@ggui-ai/iframe-runtime 0.24.0 → 0.26.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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/channels/data.d.ts +8 -0
  3. package/dist/channels/data.js +2 -0
  4. package/dist/credential-controller.d.ts +71 -0
  5. package/dist/credential-controller.js +135 -0
  6. package/dist/iframe-runtime.js +97 -93
  7. package/dist/iframe-runtime.meta.json +1 -1
  8. package/dist/index.d.ts +2 -2
  9. package/dist/ladders.d.ts +77 -0
  10. package/dist/ladders.js +383 -0
  11. package/dist/meta-parse.js +38 -8
  12. package/dist/observability.d.ts +38 -11
  13. package/dist/react-renderer.d.ts +22 -0
  14. package/dist/react-renderer.js +118 -3
  15. package/dist/registry-subscribe.d.ts +53 -2
  16. package/dist/registry-subscribe.js +63 -8
  17. package/dist/render-failure-report.d.ts +15 -0
  18. package/dist/render-failure-report.js +61 -0
  19. package/dist/render-item.d.ts +6 -1
  20. package/dist/render-item.js +7 -2
  21. package/dist/runtime-telemetry.d.ts +28 -18
  22. package/dist/runtime-telemetry.js +68 -29
  23. package/dist/runtime.d.ts +157 -39
  24. package/dist/runtime.js +724 -368
  25. package/dist/shims/wire.js +1 -1
  26. package/dist/stream-seq.d.ts +56 -0
  27. package/dist/stream-seq.js +46 -0
  28. package/dist/system-cards/ProtocolProbeCard.d.ts +16 -0
  29. package/dist/system-cards/ProtocolProbeCard.js +17 -14
  30. package/dist/tool-result-error.d.ts +6 -0
  31. package/dist/tool-result-error.js +32 -0
  32. package/dist/types.d.ts +49 -1
  33. package/dist/view-origin.d.ts +29 -0
  34. package/dist/view-origin.js +33 -0
  35. package/dist/view-proof-signer.d.ts +31 -0
  36. package/dist/view-proof-signer.js +99 -0
  37. package/dist/view-root.d.ts +51 -0
  38. package/dist/view-root.js +56 -0
  39. package/dist/wire-config.d.ts +15 -1
  40. package/dist/wire-config.js +42 -1
  41. package/package.json +8 -7
@@ -1,24 +1,58 @@
1
+ import { RUNTIME_TELEMETRY_KINDS, } from '@ggui-ai/protocol/runtime-telemetry';
1
2
  import { RUNTIME_TELEMETRY_MAX_EVENTS } from '@ggui-ai/protocol/wire';
2
3
  const FIRST_FLUSH_DELAY_MS = 4_000;
3
4
  const FLUSH_THROTTLE_MS = 8_000;
4
- const MAX_FLUSHES_PER_SESSION = 12;
5
+ const MAX_HEALTH_FLUSHES_PER_SESSION = 8;
6
+ const MAX_DIAGNOSTIC_FLUSHES_PER_SESSION = 4;
5
7
  const MAX_DETAIL_CHARS = 500;
8
+ /** The kind vocabulary (`@ggui-ai/protocol/runtime-telemetry`, ggui#1381), indexed by kind. */
9
+ const SPEC_BY_KIND = new Map(RUNTIME_TELEMETRY_KINDS.map((s) => [s.kind, s]));
10
+ const ID_DETAIL = /^[A-Za-z0-9_.:-]{1,128}$/;
11
+ function isBooleansObject(detail) {
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(detail);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
20
+ return false;
21
+ const values = Object.values(parsed);
22
+ return values.length > 0 && values.every((v) => typeof v === 'boolean');
23
+ }
24
+ /** The detail a kind may carry, or `undefined` when its shape does not admit this one. */
25
+ function admittedDetail(spec, detail) {
26
+ if (detail === undefined || spec === undefined)
27
+ return undefined;
28
+ switch (spec.detail) {
29
+ case 'none':
30
+ return undefined;
31
+ case 'id':
32
+ return ID_DETAIL.test(detail) ? detail : undefined;
33
+ case 'booleans':
34
+ return detail.length <= MAX_DETAIL_CHARS && isBooleansObject(detail) ? detail : undefined;
35
+ case 'fields':
36
+ return detail.slice(0, MAX_DETAIL_CHARS);
37
+ }
38
+ }
6
39
  export function createTelemetrySink(opts) {
7
40
  const bootAt = Date.now();
8
- const buffer = [];
9
- let flushes = 0;
41
+ const batches = {
42
+ health: { events: [], flushes: 0, maxFlushes: MAX_HEALTH_FLUSHES_PER_SESSION },
43
+ diagnostic: { events: [], flushes: 0, maxFlushes: MAX_DIAGNOSTIC_FLUSHES_PER_SESSION },
44
+ };
45
+ const order = ['health', 'diagnostic'];
10
46
  let timer = null;
11
47
  let disposed = false;
12
48
  let firstFlushDone = false;
13
- const flush = () => {
14
- timer = null;
15
- if (disposed || buffer.length === 0)
16
- return;
17
- if (flushes >= MAX_FLUSHES_PER_SESSION)
49
+ const canFlush = (b) => batches[b].events.length > 0 && batches[b].flushes < batches[b].maxFlushes;
50
+ const send = (b) => {
51
+ if (!canFlush(b))
18
52
  return;
19
- flushes += 1;
20
- firstFlushDone = true;
21
- const events = buffer.splice(0, buffer.length);
53
+ const batch = batches[b];
54
+ batch.flushes += 1;
55
+ const events = batch.events.splice(0, batch.events.length);
22
56
  void opts
23
57
  .callTool({
24
58
  name: 'ggui_runtime_telemetry',
@@ -29,39 +63,44 @@ export function createTelemetrySink(opts) {
29
63
  // cause the failures it exists to report.
30
64
  });
31
65
  };
66
+ const flush = () => {
67
+ timer = null;
68
+ if (disposed)
69
+ return;
70
+ firstFlushDone = true;
71
+ for (const b of order)
72
+ send(b);
73
+ };
32
74
  const scheduleFlush = () => {
33
75
  if (disposed || timer !== null)
34
76
  return;
35
- if (flushes >= MAX_FLUSHES_PER_SESSION)
77
+ if (!order.some(canFlush))
36
78
  return;
37
79
  timer = setTimeout(flush, firstFlushDone ? FLUSH_THROTTLE_MS : FIRST_FLUSH_DELAY_MS);
38
80
  };
39
- const record = (kind, detail) => {
81
+ const append = (kind, detail) => {
40
82
  if (disposed)
41
83
  return;
42
- if (buffer.length >= RUNTIME_TELEMETRY_MAX_EVENTS)
43
- buffer.shift();
44
- buffer.push({
84
+ const spec = SPEC_BY_KIND.get(kind);
85
+ // An unlisted kind (a caller past the types) travels as diagnostic, and without its detail.
86
+ const batch = batches[spec?.batch ?? 'diagnostic'];
87
+ if (batch.events.length >= RUNTIME_TELEMETRY_MAX_EVENTS)
88
+ batch.events.shift();
89
+ const kept = admittedDetail(spec, detail);
90
+ batch.events.push({
45
91
  at: Math.max(0, Date.now() - bootAt),
46
92
  kind: kind.slice(0, 64),
47
- ...(detail !== undefined ? { detail: detail.slice(0, MAX_DETAIL_CHARS) } : {}),
93
+ ...(kept !== undefined ? { detail: kept } : {}),
48
94
  });
49
95
  scheduleFlush();
50
96
  };
51
- const fieldsToDetail = (fields) => {
52
- try {
53
- return JSON.stringify(fields).slice(0, MAX_DETAIL_CHARS);
54
- }
55
- catch {
56
- return '[unserializable]';
57
- }
58
- };
97
+ const onChannelEvent = (event) => append(event, undefined);
59
98
  return {
60
- record,
99
+ record: (kind, detail) => append(kind, detail),
61
100
  channelLogger: {
62
- info: (event, fields) => record(event, fieldsToDetail(fields)),
63
- warn: (event, fields) => record(event, fieldsToDetail(fields)),
64
- debug: (event, fields) => record(event, fieldsToDetail(fields)),
101
+ info: onChannelEvent,
102
+ warn: onChannelEvent,
103
+ debug: onChannelEvent,
65
104
  },
66
105
  dispose() {
67
106
  disposed = true;
package/dist/runtime.d.ts CHANGED
@@ -29,14 +29,17 @@ import type { DrainAckPayload, JsonValue, JsonObject, GguiSession } from '@ggui-
29
29
  import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
30
30
  import { type McpAppAiGguiRenderMeta, type McpAppBootstrapFailedMessage, type McpAppRendererReadyMessage } from '@ggui-ai/protocol/integrations/mcp-apps';
31
31
  import type { GguiSessionSeedInput } from './types.js';
32
- import type { McpAppAiGguiMetaParseFailureReason } from './types.js';
32
+ import type { HeldCredential, McpAppAiGguiMetaParseFailureReason, ViewRoot } from './types.js';
33
+ import type { ViewRootHolder } from './view-root.js';
34
+ import { type StreamSeqTracker } from './stream-seq.js';
33
35
  import { App } from '@modelcontextprotocol/ext-apps';
34
36
  import type { McpUiDisplayMode } from '@modelcontextprotocol/ext-apps';
35
37
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
36
38
  import type { GadgetPackageRegistry } from './globals.js';
37
39
  import { StreamBus } from './wire-config.js';
38
40
  import { type ChannelTransportRouter } from './channel-transport.js';
39
- import { ChannelRegistry } from '@ggui-ai/live-channel';
41
+ import type { ChannelHandler } from '@ggui-ai/live-channel';
42
+ import { type TelemetrySink } from './runtime-telemetry.js';
40
43
  import { type ConnectFn } from './registry-subscribe.js';
41
44
  import { type StatusRefs } from './status-dom.js';
42
45
  import { type RendererValidatorContext } from './validation.js';
@@ -44,6 +47,7 @@ import type { WireConfig } from '@ggui-ai/wire';
44
47
  import { type ProtocolErrorEmitter } from './protocol-error.js';
45
48
  import { type ObservabilityEmitter } from './observability.js';
46
49
  import { type LifecycleEmitter } from './lifecycle.js';
50
+ import { type ContextSnapshotPoster } from './context-observer.js';
47
51
  /**
48
52
  * Drain-ack listener registry. Module-scoped so the dispatch path can
49
53
  * register a frame-listener without threading it through the renderer.
@@ -92,12 +96,10 @@ export declare function setCurrentApp(app: App): void;
92
96
  * the spec's `'light' | 'dark'` literals (dropped, never guessed —
93
97
  * the same tolerant posture as `projectHostContext`).
94
98
  *
95
- * This is the INPUT leg of host-theme adaptation
96
- * (rnd/gen-ui/beauty/experiments/002-host-theme-adaptation.md, ggui#551):
99
+ * This is the INPUT leg of host-theme adaptation (ggui#551):
97
100
  * `hostContext.theme` alone reaches only `data-theme` +
98
101
  * `color-scheme` via `applyDocumentTheme` — nothing in the token
99
- * pipeline keys off it, so a dark host painted the light ladder
100
- * (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
102
+ * pipeline keys off it, so a dark host painted the light ladder.
101
103
  * Under ggui#987 §4 the embedding host OWNS runtime mode: when it
102
104
  * announces one, `buildOpts` paints it over every stamped opinion; the
103
105
  * stamped layers decide only when the host is silent. Read live from
@@ -338,8 +340,10 @@ export interface BootSequenceOptions {
338
340
  * canonical async toolresult) are skipped. The App handshake still
339
341
  * runs — spec mandates `ui/initialize` regardless of how slice meta
340
342
  * arrives, and `hostContext` is captured from `app.getHostContext()`.
343
+ * The slice's held credential, when it has one, is refreshed exactly
344
+ * as a slice this boot resolved itself (ggui#1496).
341
345
  */
342
- readonly preResolvedMeta?: McpAppAiGguiRenderMeta;
346
+ readonly preResolved?: BootMeta;
343
347
  /**
344
348
  * Autostart-layer pre-resolution of the read-plane door (ggui#537):
345
349
  * a tool result that arrived before `bootSequence` ran carried the
@@ -348,9 +352,17 @@ export interface BootSequenceOptions {
348
352
  * before the App is connected, so the locator is threaded here and
349
353
  * resolved via {@link resolveMetaViaReadDoor} right after the
350
354
  * handshake, ahead of the Tier 2 tool-result wait. Ignored when
351
- * `preResolvedMeta` is set.
355
+ * `preResolved` is set.
352
356
  */
353
357
  readonly preResolvedLocator?: string;
358
+ /**
359
+ * Test seam for the boot refresh's waits (ggui#1496): the retry delay's
360
+ * `sleep` and `random`. Defaults to a `setTimeout` wait and `Math.random`.
361
+ */
362
+ readonly credentialTiming?: {
363
+ readonly sleep?: (ms: number) => Promise<void>;
364
+ readonly random?: () => number;
365
+ };
354
366
  /**
355
367
  * How long to wait for the spec-canonical `ui/notifications/tool-result`
356
368
  * notification (Tier 2 of the resolver chain) before failing with
@@ -359,6 +371,12 @@ export interface BootSequenceOptions {
359
371
  * timeout so the spec doesn't hang.
360
372
  */
361
373
  readonly toolResultTimeoutMs?: number;
374
+ /**
375
+ * Test seam for the view's key root (ggui#1415): the holder the boot and
376
+ * every later tool result offer their slice's root to. Defaults to the
377
+ * document's one holder.
378
+ */
379
+ readonly viewRoots?: ViewRootHolder;
362
380
  }
363
381
  /**
364
382
  * Renderer hooks. The real iframe boot plumbs these via
@@ -450,20 +468,23 @@ export interface RendererHandle {
450
468
  */
451
469
  readonly channelTransport: ChannelTransportRouter;
452
470
  /**
453
- * Channel-client registry holding handlers for every WS frame type
454
- * the iframe routes (`render`, `data`, `props_update`, `drain_ack`,
455
- * `channel_payload`, `channel_error`). The
456
- * registry-bound transport is the sole dispatch surface — frames
457
- * arrive directly through registered handlers, no longer through a
471
+ * The handlers for every frame type the iframe routes (`render`,
472
+ * `data`, `props_update`, `drain_ack`, `channel_payload`,
473
+ * `channel_error`). Frames arrive directly through them, not through a
458
474
  * separate `onMessage` callback.
459
475
  *
460
- * `bootSequence` calls `registry.bind(...)` indirectly through the
461
- * `connectFn` seam after `setup()` returns. Post-bind, registration
462
- * of new handlers is frozen (the registry guards against it); the
463
- * `subscribe-handshake` handlers (`ack`, `error`) are added by
464
- * `connectViaRegistry` and consumed during handshake resolution.
476
+ * `bootSequence`'s ladder set (ggui#1496) registers these same objects
477
+ * on each ladder's registry, one per credential, behind a gate that
478
+ * lets only the ladder holding the data deliver. `connectViaRegistry`
479
+ * adds the per-ladder `subscribe-handshake` handlers (`ack`, `error`).
465
480
  */
466
- readonly channelRegistry: ChannelRegistry;
481
+ readonly channelHandlers: readonly ChannelHandler[];
482
+ /**
483
+ * The view's stream cursor (ggui#1496): the highest stamped `data`
484
+ * envelope `seq` applied. The `data` handler dedupes on it, and a new
485
+ * subscribe resumes the stream from it (`fromSeq`).
486
+ */
487
+ readonly streamSeq: StreamSeqTracker;
467
488
  /**
468
489
  * 3rd-party gadget-package merge promise. `installGlobalRegistry`
469
490
  * seeds `__ggui__.gadgets` synchronously with the STDLIB namespace;
@@ -489,18 +510,35 @@ export interface BootSequenceResult {
489
510
  readonly mountedRender: GguiSession | GguiSessionSeedInput | null;
490
511
  }
491
512
  export declare function bootSequence(opts: BootSequenceOptions): Promise<BootSequenceResult>;
513
+ /**
514
+ * A resolved boot slice (ggui#1496): the meta to mount, and the expired
515
+ * live credential the parse held back from it, when there was one. Every
516
+ * boot resolver returns this, so the held credential survives every path
517
+ * to {@link bootSequence}, which refreshes it once through the host.
518
+ */
519
+ export interface BootMeta {
520
+ readonly meta: McpAppAiGguiRenderMeta;
521
+ readonly held?: HeldCredential;
522
+ /** The slice's view key root, when it carries one (ggui#1415). */
523
+ readonly viewRoot?: ViewRoot;
524
+ }
525
+ /**
526
+ * The bridge rung's `tools/call` over the host relay (ggui#1415): every
527
+ * `ggui_runtime_pull` it sends goes through {@link withViewProof}.
528
+ *
529
+ * @internal — exported for unit tests + production reuse.
530
+ */
531
+ export declare function bridgeCallToolVia(app: App): (name: string, args: Record<string, unknown>) => ReturnType<App['callServerTool']>;
492
532
  /**
493
533
  * Read `globalThis.__GGUI_META__` synchronously, validate against
494
- * the unified slice-meta shape, and return the typed meta (or null
534
+ * the unified slice-meta shape, and return the boot slice (or null
495
535
  * on absence / malformation).
496
536
  *
497
- * Thin wrapper around {@link parseMetaFromGlobal}; preserved for
498
- * back-compat (downstream consumers + tests). Returns `null` instead
499
- * of {@link McpAppAiGguiMetaParseResult} to match the historical
500
- * reader contract — the autostart resolver only needs the "valid
501
- * meta or fall through" signal.
537
+ * Thin wrapper around {@link parseMetaFromGlobal}. Returns `null`
538
+ * instead of {@link McpAppAiGguiMetaParseResult} because the autostart
539
+ * resolver only needs the "a slice to boot, or fall through" signal.
502
540
  */
503
- export declare function readSelfContainedMeta(): McpAppAiGguiRenderMeta | null;
541
+ export declare function readSelfContainedMeta(): BootMeta | null;
504
542
  /**
505
543
  * Project the inline `__GGUI_META__` bootstrap into a
506
544
  * {@link GguiSessionSeedInput} the mount surface can paint immediately,
@@ -566,7 +604,7 @@ export declare function hasStaticContentMeta(meta: McpAppAiGguiRenderMeta): bool
566
604
  * lagged the protocol" bug class: shell + runtime drift becomes
567
605
  * impossible because the shell never inspects the slice-meta shape.
568
606
  */
569
- export declare function extractMetaFromToolResult(params: unknown): McpAppAiGguiRenderMeta | null;
607
+ export declare function extractMetaFromToolResult(params: unknown): BootMeta | null;
570
608
  /**
571
609
  * Drain `window.__GGUI_PENDING_TOOL_RESULTS__` — the buffer the
572
610
  * minimal shell populates while messages arrive between
@@ -585,7 +623,7 @@ export declare function extractMetaFromToolResult(params: unknown): McpAppAiGgui
585
623
  * notifications, in arrival order (shells cap it newest-biased). The
586
624
  * runtime reads it once at autostart.
587
625
  */
588
- export declare function readPendingToolResults(): McpAppAiGguiRenderMeta | null;
626
+ export declare function readPendingToolResults(): BootMeta | null;
589
627
  /**
590
628
  * Sibling of {@link readPendingToolResults} for the read-plane-only
591
629
  * posture (ggui#537): the NEWEST buffered tool result that carries a
@@ -608,20 +646,20 @@ export declare function readPendingToolResultLocator(): string | null;
608
646
  * slice would be. `null` on any miss (host cannot proxy reads, resource
609
647
  * gone, no envelope) — the caller's other tiers still decide.
610
648
  */
611
- export declare function resolveMetaViaReadDoor(app: App, locator: string): Promise<McpAppAiGguiRenderMeta | null>;
649
+ export declare function resolveMetaViaReadDoor(app: App, locator: string): Promise<BootMeta | null>;
612
650
  /**
613
- * Render a submit-action's `data` payload as a short inline string
614
- * for embedding in a `ui/message` consent prompt. Goal: human-
615
- * readable, not a JSON dump. Falls back to truncated JSON for
616
- * nested values so the prompt doesn't drop information silently.
651
+ * Production {@link ContextSnapshotPoster} — the seam the
652
+ * context-observer factories consume. Splits the two destinations:
617
653
  *
618
- * Exported for unit-testing — an earlier implementation returned `''`
619
- * for primitive payloads (strings/numbers/booleans), which silently
620
- * vaporised the chip's actual text from the consent prompt and made
621
- * the LLM think every dispatch was a contentless "Please proceed
622
- * with **<intent>**" request.
654
+ * - `postUpdateModelContext` → spec-canonical
655
+ * `app.updateModelContext(...)` (via {@link callAppUpdateModelContext}).
656
+ * - `postContextMirror` → raw `tools/call ggui_runtime_sync_context`
657
+ * via {@link postToParent} (the host-relay mirror path; not yet
658
+ * migrated to `app.callServerTool` — see emitAudit for the same
659
+ * posture).
623
660
  */
624
- export declare function formatSubmitActionDataInline(data: unknown): string;
661
+ /** @internal — exported for unit tests. */
662
+ export declare const productionContextSnapshotPoster: ContextSnapshotPoster;
625
663
  /**
626
664
  * Fire a single canonical action envelope (`tools/call
627
665
  * ggui_runtime_submit_action`) carrying the typed `{kind, payload, …}` shape
@@ -683,6 +721,76 @@ interface ToastLiveRegions {
683
721
  * @internal — exported for unit tests.
684
722
  */
685
723
  export declare function ensureToastAnnouncer(doc: Document): ToastLiveRegions | null;
724
+ /**
725
+ * The drawn toast for a dispatched action: a fixed-position element at
726
+ * the bottom of the iframe, drawn only when the visitor has to know or do
727
+ * something (the copy is {@link GESTURE_COPY}). Two kinds reach it:
728
+ * - `'error'` — the gesture did not go through: the relay refused it or
729
+ * could not be reached, or the card's own contract refused it. Nothing
730
+ * reached the pipe, so no `ui/message` doorbell fires (SPEC §4.7
731
+ * "Failure-soft client posture");
732
+ * - `'action_required'` — the gesture is queued but no consumer is
733
+ * listening and this host cannot take (or refused) the wake-up, so the
734
+ * visitor must send a message to continue.
735
+ * A tap that went through, or a wake-up the host took, is spoken, never
736
+ * drawn.
737
+ *
738
+ * Without it, a failed gesture would be silent: the visitor could not tell
739
+ * "the agent is busy" from "the tap was lost".
740
+ *
741
+ * Direct DOM (not React-managed) because:
742
+ * - Works during boot before React mounts.
743
+ * - Doesn't interfere with the component tree the generator owns.
744
+ * - Survives React mount transitions inside the iframe.
745
+ *
746
+ * Single global toast (per iframe). Auto-dismisses after 2.5s on
747
+ * `success` / `fallback` outcomes. A successful dispatch draws nothing
748
+ * here (ggui#1444): its "sending" is spoken only (see
749
+ * {@link GESTURE_COPY}).
750
+ *
751
+ * The visible element is only half the surface (ggui#447). Every
752
+ * message is also spoken through {@link ensureToastAnnouncer}'s live
753
+ * regions, and the element itself is hidden from the accessibility
754
+ * tree so the sentence is not read twice. The one exception is
755
+ * `action_required`, the only toast the user must OPERATE: it re-enters
756
+ * the tree as a named button with a tab stop, because a dismissal that
757
+ * needs a mouse is a dismissal some users cannot perform.
758
+ *
759
+ * @internal — runtime-layer concern.
760
+ */
761
+ /**
762
+ * The `label` an action declares on the render's `actionSpec`, trimmed, or
763
+ * `undefined` when the render declares none (or is a system / MCP-Apps
764
+ * render, which carries no actionSpec). By contract the label is the
765
+ * control's own copy, so it is the one name for a tap a visitor may read
766
+ * (see {@link GESTURE_COPY}).
767
+ *
768
+ * @internal — exported for unit tests.
769
+ */
770
+ export declare function declaredActionLabel(render: GguiSession | GguiSessionSeedInput | null, actionName: string): string | undefined;
771
+ /** @internal — exported for unit tests. */
772
+ export declare function isActionSpentByRefusal(renderId: string, actionName: string): boolean;
773
+ /**
774
+ * ggui#1424 — a post-ack `error` frame from the live channel. The channel
775
+ * door refuses a second gesture on a spent one-shot with its
776
+ * CONTRACT_VIOLATION frame and a violation at `actionSpec.<name>.oneShot`;
777
+ * that one means done, the same as the tool answer. Any other error frame
778
+ * changes nothing here.
779
+ *
780
+ * @internal — exported for unit tests.
781
+ */
782
+ export declare function handlePostAckErrorFrame(renderId: string, payload: {
783
+ readonly code: string;
784
+ readonly message?: string;
785
+ readonly details?: unknown;
786
+ }): void;
787
+ /**
788
+ * The toast for a tap the outbound contract check refused (ggui#1536): the
789
+ * ordinary error toast, named by the action's declared label.
790
+ *
791
+ * @internal — exported for unit tests + production reuse.
792
+ */
793
+ export declare function showRefusedDispatchToast(label: string | undefined): void;
686
794
  /**
687
795
  * Boot-time reset of the relay latch (ggui#670 Phase 3). A latch
688
796
  * standing from a prior mount in a re-booted document is closed through
@@ -694,6 +802,12 @@ export declare function ensureToastAnnouncer(doc: Document): ToastLiveRegions |
694
802
  */
695
803
  export declare function resetRelayLatchForBoot(): void;
696
804
  /** @internal — exported for unit tests to reset module state. */
805
+ /**
806
+ * Test-only: install the telemetry sink the runtime records into (normally
807
+ * set once at boot), so a test can observe the events a code path posts.
808
+ * `null` restores the no-sink state.
809
+ */
810
+ export declare function __setTelemetrySinkForTest(sink: TelemetrySink | null): void;
697
811
  export declare function __resetRelayNoticeForTest(): void;
698
812
  /**
699
813
  * Dispatch a submit-action via the empirically-validated bridge
@@ -714,6 +828,8 @@ export declare function dispatchSubmitAction(args: {
714
828
  readonly data: unknown;
715
829
  readonly sessionId: string;
716
830
  readonly appId: string;
831
+ /** The action's declared `label`, when it has one — see {@link GESTURE_COPY}. */
832
+ readonly label?: string;
717
833
  }): void;
718
834
  /**
719
835
  * Channel-transport router's `tools/call` invoker (ggui#440) —
@@ -809,6 +925,8 @@ export declare function routeDispatch(args: {
809
925
  readonly appId: string;
810
926
  };
811
927
  readonly dispatchToolName: string;
928
+ /** The action's declared `label` (visitor copy by contract), when it has one. */
929
+ readonly label?: string;
812
930
  }): void;
813
931
  /**
814
932
  * `ui/open-link` direct dispatch helper — used by the anchor-click