runcloud 0.1.115 → 0.1.117

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/README.md CHANGED
@@ -66,6 +66,7 @@ runcloud ios get <id> # inspect a session (
66
66
  runcloud ios open-url myapp://path --id <id> # open a URL or deep link
67
67
  runcloud ios tap <id> 0.5 0.75 # normalized top-left display coordinates
68
68
  runcloud ios type-text <id> "Hello from iOS!" # printable US ASCII, tab, and line feed
69
+ runcloud ios accessibility-tree <id> --json # read the current typed accessibility hierarchy
69
70
  runcloud ios screenshot <id> --output ios.png # save a PNG without exposing the viewer URL
70
71
  runcloud ios recording start <id> --json # start an idempotent MP4 screen recording
71
72
  runcloud ios recording stop <id> <recording-id> --json # stop and finalize it
@@ -79,9 +80,27 @@ runcloud ios delete <id> # release the session
79
80
  runcloud android create --model pixel
80
81
  runcloud android swipe <id> 0.5 0.8 0.5 0.2 --duration 250
81
82
  runcloud android press-key <id> enter
83
+ runcloud android accessibility-tree <id> --json
82
84
  runcloud android screenshot <id> --output android.png --json
83
85
  ```
84
86
 
87
+ `open-url` accepts HTTPS URLs and application deep links on both platforms. Keep
88
+ the complete target quoted so its URI delimiters stay in one argument:
89
+
90
+ ```bash
91
+ runcloud ios open-url \
92
+ 'runcloudproof://open/items%2F42?message=hello%20world&return=https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof' \
93
+ --id <id> \
94
+ --json
95
+ ```
96
+
97
+ Success returns `ok`, `platform`, `sessionId`, `device`, `leaseId`, and the
98
+ exact input `url`. Replace `ios` with `android` for an emulator. See
99
+ [Open URLs and Deep Links](https://docs.run.cloud/platform/open-urls) for SDK
100
+ and REST examples, platform behavior, validation, and troubleshooting.
101
+ On iOS, a first custom-scheme handoff can still require confirming the system's
102
+ **Open in “App”?** prompt after the success acknowledgement.
103
+
85
104
  Useful `create` flags: `--region`, `--display-name`, `--inactivity-timeout 3m`,
86
105
  `--hard-timeout 1h`, `--install-asset <name-or-id>`, `--rm` (release when the
87
106
  command exits), `--json`.
@@ -100,6 +119,12 @@ report `capsLock`, `numLock`, and `scrollLock` as unsupported keys.
100
119
  Coordinates are inclusive normalized display coordinates: `(0, 0)` is the
101
120
  top-left and `(1, 1)` is the bottom-right.
102
121
 
122
+ `accessibility-tree` (`accessibility` alias) returns an indented terminal tree
123
+ or a versioned JSON hierarchy with cross-platform roles, labels, values,
124
+ states, bounds, identifiers, and nested children. Secure field values are
125
+ always redacted. The command reads only the authenticated active session and
126
+ never releases it; use `--timeout` to change the 20-second client wait.
127
+
103
128
  The `recording` group is shared by iOS and Android. It provides `start`,
104
129
  `list`, `status` (`get` alias), `stop`, and `download`. Pass a stable
105
130
  `--idempotency-key` to retry `start` without creating a second recording.
package/dist/api.js CHANGED
@@ -10,6 +10,7 @@ export class ApiError extends Error {
10
10
  sessionId;
11
11
  platform;
12
12
  action;
13
+ suggestedAction;
13
14
  interactionStatus;
14
15
  acceptedAt;
15
16
  completedAt;
@@ -65,6 +66,7 @@ function parsedError(body) {
65
66
  ...(typeof parsed.sessionId === 'string' ? { sessionId: parsed.sessionId } : {}),
66
67
  ...(platform ? { platform } : {}),
67
68
  ...(typeof parsed.action === 'string' ? { action: parsed.action } : {}),
69
+ ...(typeof nested?.action === 'string' ? { suggestedAction: nested.action } : {}),
68
70
  ...(interactionStatus ? { interactionStatus } : {}),
69
71
  ...(typeof parsed.acceptedAt === 'string' ? { acceptedAt: parsed.acceptedAt } : {}),
70
72
  ...(typeof parsed.completedAt === 'string' ? { completedAt: parsed.completedAt } : {}),
@@ -114,8 +116,8 @@ export class ApiClient {
114
116
  const contentType = res.headers.get('content-type') ?? '';
115
117
  return contentType.includes('application/json') ? res.json() : res;
116
118
  }
117
- get(path) {
118
- return this.request('GET', path);
119
+ get(path, options = {}) {
120
+ return this.request('GET', path, undefined, options);
119
121
  }
120
122
  async getText(path) {
121
123
  const response = await this.request('GET', path);
@@ -53,8 +53,19 @@ export const SIMULATOR_SCREENSHOT_ERROR_CODES = [
53
53
  'screenshot_invalid_response',
54
54
  'screenshot_failed',
55
55
  ];
56
+ export const SIMULATOR_ACCESSIBILITY_ERROR_CODES = [
57
+ 'active_session_not_found',
58
+ 'simulator_capacity_unavailable',
59
+ 'simulator_session_ended',
60
+ 'accessibility_unavailable',
61
+ 'accessibility_cancelled',
62
+ 'accessibility_timeout',
63
+ 'accessibility_transport_error',
64
+ 'accessibility_invalid_response',
65
+ ];
56
66
  const simulatorInteractionErrorCodeSet = new Set(SIMULATOR_INTERACTION_ERROR_CODES);
57
67
  const simulatorScreenshotErrorCodeSet = new Set(SIMULATOR_SCREENSHOT_ERROR_CODES);
68
+ const simulatorAccessibilityErrorCodeSet = new Set(SIMULATOR_ACCESSIBILITY_ERROR_CODES);
58
69
  function normalizedInteractionErrorCode(value) {
59
70
  if (typeof value === 'string' && simulatorInteractionErrorCodeSet.has(value)) {
60
71
  return value;
@@ -85,7 +96,13 @@ function normalizedScreenshotErrorCode(value) {
85
96
  ? value
86
97
  : 'screenshot_failed';
87
98
  }
99
+ function normalizedAccessibilityErrorCode(value) {
100
+ return typeof value === 'string' && simulatorAccessibilityErrorCodeSet.has(value)
101
+ ? value
102
+ : 'accessibility_unavailable';
103
+ }
88
104
  const DEFAULT_INTERACTION_TIMEOUT_MS = 15_000;
105
+ const DEFAULT_ACCESSIBILITY_TIMEOUT_MS = 20_000;
89
106
  const INTERACTION_RESPONSE_MARGIN_MS = 250;
90
107
  const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
91
108
  export const RUN_CLOUD_SKILL_NAMES = [
@@ -392,10 +409,21 @@ class CliInteractionError extends Error {
392
409
  }
393
410
  }
394
411
  async function withInteractionCancellation(timeoutMs, requestId, actionName, sessionId, platform, operation, run) {
412
+ const operationLabel = operation === 'interaction' ? 'interaction' : operation;
413
+ const cancelledCode = operation === 'screenshot'
414
+ ? 'screenshot_cancelled'
415
+ : operation === 'accessibility'
416
+ ? 'accessibility_cancelled'
417
+ : 'interaction_cancelled';
418
+ const timeoutCode = operation === 'screenshot'
419
+ ? 'screenshot_timeout'
420
+ : operation === 'accessibility'
421
+ ? 'accessibility_timeout'
422
+ : 'interaction_timeout';
395
423
  const controller = new AbortController();
396
- const onSigint = () => controller.abort(new CliInteractionError(`Simulator ${operation === 'screenshot' ? 'screenshot' : 'interaction'} cancelled by SIGINT`, operation === 'screenshot' ? 'screenshot_cancelled' : 'interaction_cancelled', requestId, actionName, sessionId, platform));
424
+ const onSigint = () => controller.abort(new CliInteractionError(`Simulator ${operationLabel} cancelled by SIGINT`, cancelledCode, requestId, actionName, sessionId, platform));
397
425
  process.once('SIGINT', onSigint);
398
- const timer = setTimeout(() => controller.abort(new CliInteractionError(`Simulator ${actionName} did not complete within ${timeoutMs} ms`, operation === 'screenshot' ? 'screenshot_timeout' : 'interaction_timeout', requestId, actionName, sessionId, platform)), timeoutMs + INTERACTION_RESPONSE_MARGIN_MS);
426
+ const timer = setTimeout(() => controller.abort(new CliInteractionError(`Simulator ${actionName} did not complete within ${timeoutMs} ms`, timeoutCode, requestId, actionName, sessionId, platform)), timeoutMs + INTERACTION_RESPONSE_MARGIN_MS);
399
427
  try {
400
428
  return await run(controller.signal);
401
429
  }
@@ -485,6 +513,143 @@ async function captureSimulatorScreenshot(platform, id, opts) {
485
513
  else
486
514
  console.log(`Saved ${platform} simulator screenshot to ${output} (${bytes.byteLength} bytes)`);
487
515
  }
516
+ function validatedAccessibilitySnapshot(value, platform) {
517
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
518
+ throw new Error('accessibility response must be an object');
519
+ }
520
+ const snapshot = value;
521
+ if (snapshot.schemaVersion !== 1 || snapshot.platform !== platform) {
522
+ throw new Error('accessibility schema or platform did not match the request');
523
+ }
524
+ if (!Array.isArray(snapshot.roots))
525
+ throw new Error('accessibility roots must be an array');
526
+ if (typeof snapshot.nodeCount !== 'number'
527
+ || !Number.isInteger(snapshot.nodeCount)
528
+ || snapshot.nodeCount < 0
529
+ || snapshot.nodeCount > 500) {
530
+ throw new Error('accessibility nodeCount must be an integer from 0 to 500');
531
+ }
532
+ if (!snapshot.screen || typeof snapshot.screen !== 'object' || Array.isArray(snapshot.screen)) {
533
+ throw new Error('accessibility screen must be an object');
534
+ }
535
+ const screen = snapshot.screen;
536
+ if (screen.unit !== (platform === 'ios' ? 'points' : 'pixels')) {
537
+ throw new Error('accessibility screen unit did not match the platform');
538
+ }
539
+ const ids = new Set();
540
+ let count = 0;
541
+ const visit = (candidate, path, depth) => {
542
+ if (depth > 80 || count >= 500 || !candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
543
+ throw new Error('accessibility tree exceeded its schema bounds');
544
+ }
545
+ const node = candidate;
546
+ if (typeof node.id !== 'string' || !node.id || ids.has(node.id)) {
547
+ throw new Error('accessibility node identifiers must be non-empty and unique');
548
+ }
549
+ if (node.path !== path || typeof node.role !== 'string' || !Array.isArray(node.children)) {
550
+ throw new Error('accessibility node structure is invalid');
551
+ }
552
+ const states = node.states && typeof node.states === 'object' && !Array.isArray(node.states)
553
+ ? node.states
554
+ : null;
555
+ const native = node.native && typeof node.native === 'object' && !Array.isArray(node.native)
556
+ ? node.native
557
+ : null;
558
+ if (!states || !native || native.platform !== platform) {
559
+ throw new Error('accessibility node state or platform detail is invalid');
560
+ }
561
+ const secure = states.secure === true
562
+ || native.password === true
563
+ || (platform === 'ios' && /(?:secure|password)/iu.test([
564
+ native.type,
565
+ native.roleDescription,
566
+ native.subrole,
567
+ ].filter((part) => typeof part === 'string').join(' ')));
568
+ if (secure) {
569
+ states.secure = true;
570
+ node.value = null;
571
+ }
572
+ ids.add(node.id);
573
+ count += 1;
574
+ node.children.forEach((child, index) => visit(child, `${path}.${index}`, depth + 1));
575
+ };
576
+ snapshot.roots.forEach((root, index) => visit(root, String(index), 0));
577
+ if (count !== snapshot.nodeCount)
578
+ throw new Error('accessibility nodeCount did not match the tree');
579
+ return snapshot;
580
+ }
581
+ function printAccessibilitySnapshot(snapshot) {
582
+ const platform = String(snapshot.platform);
583
+ const screen = snapshot.screen;
584
+ console.log(`${platform} accessibility tree: ${String(snapshot.nodeCount)} nodes, `
585
+ + `${String(screen.width)}x${String(screen.height)} ${String(screen.unit)}`
586
+ + `${snapshot.truncated === true ? ' (truncated)' : ''}`);
587
+ const visit = (candidate, depth) => {
588
+ const node = candidate;
589
+ const states = node.states;
590
+ const bounds = node.bounds && typeof node.bounds === 'object'
591
+ ? node.bounds
592
+ : null;
593
+ const stateLabels = [
594
+ states.enabled === false ? 'disabled' : null,
595
+ states.focused === true ? 'focused' : null,
596
+ states.selected === true ? 'selected' : null,
597
+ states.checked === true ? 'checked' : states.checked === 'mixed' ? 'mixed' : null,
598
+ states.expanded === true ? 'expanded' : states.expanded === false ? 'collapsed' : null,
599
+ states.secure === true ? 'secure' : null,
600
+ ].filter(Boolean);
601
+ const label = typeof node.label === 'string' && node.label ? ` "${node.label}"` : '';
602
+ const identifier = typeof node.identifier === 'string' && node.identifier
603
+ ? ` #${node.identifier}`
604
+ : '';
605
+ const value = typeof node.value === 'string' && node.value
606
+ ? ` = ${JSON.stringify(node.value.slice(0, 120))}`
607
+ : '';
608
+ const frame = bounds
609
+ ? ` @ ${String(bounds.x)},${String(bounds.y)} ${String(bounds.width)}x${String(bounds.height)}`
610
+ : '';
611
+ const state = stateLabels.length ? ` [${stateLabels.join(', ')}]` : '';
612
+ console.log(`${' '.repeat(depth)}${String(node.role)}${label}${identifier}${value}${state}${frame}`);
613
+ for (const child of node.children)
614
+ visit(child, depth + 1);
615
+ };
616
+ for (const root of snapshot.roots)
617
+ visit(root, 0);
618
+ }
619
+ async function captureSimulatorAccessibilityTree(platform, id, opts) {
620
+ const timeoutMs = integerInRange(opts.timeout, '--timeout', 100, 60_000, DEFAULT_ACCESSIBILITY_TIMEOUT_MS);
621
+ const requestId = `cli_${randomUUID()}`;
622
+ let value;
623
+ try {
624
+ value = await withInteractionCancellation(timeoutMs, requestId, 'accessibility', id, platform, 'accessibility', async (signal) => await client().get(`/run-cloud/${platform}/${encodeURIComponent(id)}/accessibility`, { signal }));
625
+ }
626
+ catch (error) {
627
+ if (error instanceof ApiError) {
628
+ Object.assign(error, {
629
+ code: normalizedAccessibilityErrorCode(error.code),
630
+ requestId: error.requestId ?? requestId,
631
+ sessionId: error.sessionId ?? id,
632
+ platform: error.platform ?? platform,
633
+ action: error.action ?? 'accessibility',
634
+ });
635
+ }
636
+ if (error instanceof ApiTransportError) {
637
+ throw new CliInteractionError('Simulator accessibility could not reach the run.cloud API', 'accessibility_transport_error', requestId, 'accessibility', id, platform);
638
+ }
639
+ throw error;
640
+ }
641
+ let snapshot;
642
+ try {
643
+ snapshot = validatedAccessibilitySnapshot(value, platform);
644
+ }
645
+ catch {
646
+ throw new CliInteractionError('Simulator accessibility returned an invalid tree', 'accessibility_invalid_response', requestId, 'accessibility', id, platform);
647
+ }
648
+ if (opts.json)
649
+ console.log(JSON.stringify(snapshot, null, 2));
650
+ else
651
+ printAccessibilitySnapshot(snapshot);
652
+ }
488
653
  function simulatorRecordingBase(platform, sessionId) {
489
654
  return `/run-cloud/${platform}/${encodeURIComponent(sessionId)}/recordings`;
490
655
  }
@@ -733,6 +898,58 @@ async function createSimulatorSession(platform, opts) {
733
898
  ...(opts.devHost ? { devHost: opts.devHost } : {}),
734
899
  });
735
900
  }
901
+ async function openSimulatorSessionUrl(platform, sessionId, url) {
902
+ const path = `/run-cloud/${platform}/${encodeURIComponent(sessionId)}/open-url`;
903
+ try {
904
+ const response = await client().post(path, { url });
905
+ const result = typeof response === 'object' && response !== null && !Array.isArray(response)
906
+ ? response
907
+ : {};
908
+ if (result.ok !== true
909
+ || result.platform !== platform
910
+ || result.sessionId !== sessionId
911
+ || typeof result.device !== 'string'
912
+ || result.device.length === 0
913
+ || typeof result.leaseId !== 'string'
914
+ || result.leaseId.length === 0
915
+ || result.url !== url) {
916
+ throw new ApiError(0, 'Simulator open URL returned an invalid acknowledgement', path, {
917
+ code: 'open_url_invalid_response',
918
+ retryable: true,
919
+ sessionId,
920
+ platform,
921
+ action: 'open-url',
922
+ });
923
+ }
924
+ return {
925
+ ok: true,
926
+ platform,
927
+ sessionId,
928
+ device: result.device,
929
+ leaseId: result.leaseId,
930
+ url,
931
+ };
932
+ }
933
+ catch (error) {
934
+ if (error instanceof ApiTransportError) {
935
+ throw new ApiError(0, 'Simulator open URL could not reach the run.cloud API', path, {
936
+ code: 'open_url_transport_error',
937
+ retryable: true,
938
+ sessionId,
939
+ platform,
940
+ action: 'open-url',
941
+ });
942
+ }
943
+ if (error instanceof ApiError) {
944
+ Object.assign(error, {
945
+ sessionId: error.sessionId ?? sessionId,
946
+ platform: error.platform ?? platform,
947
+ action: error.action ?? 'open-url',
948
+ });
949
+ }
950
+ throw error;
951
+ }
952
+ }
736
953
  function registerSimulatorCommands(program, platform) {
737
954
  const label = platform === 'android' ? 'Android emulator' : 'iOS simulator';
738
955
  const article = platform === 'android' ? 'an' : 'an';
@@ -792,7 +1009,7 @@ function registerSimulatorCommands(program, platform) {
792
1009
  .requiredOption('--id <id>', 'session id')
793
1010
  .option('--json', 'output JSON', false)
794
1011
  .action((url, opts) => action(async () => {
795
- print(await client().post(`/run-cloud/${platform}/${encodeURIComponent(opts.id)}/open-url`, { url }), opts);
1012
+ print(await openSimulatorSessionUrl(platform, opts.id, url), opts);
796
1013
  }, opts));
797
1014
  addInteractionOptions(simulator
798
1015
  .command('tap')
@@ -921,6 +1138,14 @@ function registerSimulatorCommands(program, platform) {
921
1138
  option: renderDebugOption(rawOption),
922
1139
  enabled: booleanArgument(rawEnabled, 'enabled'),
923
1140
  }, opts), opts));
1141
+ simulator
1142
+ .command('accessibility-tree')
1143
+ .alias('accessibility')
1144
+ .description(`Read the current accessibility hierarchy from ${article} active ${label} session`)
1145
+ .argument('<id>', 'session id')
1146
+ .option('--timeout <milliseconds>', 'capture timeout in milliseconds (100-60000)', String(DEFAULT_ACCESSIBILITY_TIMEOUT_MS))
1147
+ .option('--json', 'output the stable typed tree as JSON', false)
1148
+ .action((id, opts) => action(() => captureSimulatorAccessibilityTree(platform, id, opts), opts));
924
1149
  addInteractionOptions(simulator
925
1150
  .command('screenshot')
926
1151
  .description(`Capture the current display from ${article} ${label} session`)
@@ -1036,6 +1261,7 @@ function jsonErrorEnvelope(error) {
1036
1261
  retryable: error.retryable ?? (error.status === 408 || error.status === 429 || error.status >= 500),
1037
1262
  httpStatus: error.status,
1038
1263
  ...(error.details === undefined ? {} : { details: error.details }),
1264
+ ...(error.suggestedAction ? { action: error.suggestedAction } : {}),
1039
1265
  },
1040
1266
  };
1041
1267
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.115';
1
+ export const CLI_VERSION = '0.1.117';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.115",
3
+ "version": "0.1.117",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: run-cloud-ios-simulator
3
- description: Operate run.cloud iOS simulator and Android emulator sessions with the CLI or TypeScript SDK. Use for creating, installing, inspecting, reading logs, controlling, embedding, smoke-testing, connecting local Metro, taking screenshots or screen recordings, injecting iOS media, or releasing remote mobile sessions.
3
+ description: Operate run.cloud iOS simulator and Android emulator sessions with the CLI or TypeScript SDK. Use for creating, installing, inspecting accessibility or logs, controlling, embedding, smoke-testing, connecting local Metro, taking screenshots or screen recordings, injecting iOS media, or releasing remote mobile sessions.
4
4
  ---
5
5
 
6
6
  # Operate run.cloud Mobile Sessions
7
7
 
8
8
  Use the `runcloud` CLI for terminal workflows, `@run-cloud/sdk` for TypeScript,
9
- and `@run-cloud/ui` for React embeds.
9
+ and `@runcloud/ui` for React embeds.
10
10
 
11
11
  ## Authenticate
12
12
 
@@ -58,6 +58,7 @@ runcloud ios swipe "$SESSION_ID" 0.5 0.8 0.5 0.2 --duration 300 --json
58
58
  runcloud ios type-text "$SESSION_ID" 'hello' --json
59
59
  runcloud ios press-key "$SESSION_ID" enter --json
60
60
  runcloud ios press-button "$SESSION_ID" home --json
61
+ runcloud ios accessibility-tree "$SESSION_ID" --json
61
62
  runcloud ios screenshot "$SESSION_ID" --output ios.png --json
62
63
  ```
63
64
 
@@ -83,6 +84,15 @@ Android and returns a non-retryable `unsupported_action` on iOS.
83
84
  An acknowledgement means input dispatch completed. Confirm visible app effects
84
85
  with a screenshot or the signed viewer when the outcome matters.
85
86
 
87
+ ## Inspect Accessibility
88
+
89
+ Use `runcloud ios|android accessibility-tree <id> --json` to read the current
90
+ nested role, label, value, state, bounds, identifier, and child hierarchy. Read
91
+ again after a control when the task depends on a state or navigation change.
92
+ Check `truncated` before asserting that an element is absent. Prefer app-owned
93
+ identifiers when available; generated node ids and paths can change with the
94
+ hierarchy. Secure field values are always `null`.
95
+
86
96
  ## Record the Screen
87
97
 
88
98
  Both platforms expose the same session-scoped recording lifecycle:
@@ -137,6 +147,7 @@ typed acknowledgements. Use `RunCloudError` fields such as `code`, `retryable`,
137
147
 
138
148
  The lifecycle surface also includes `create`, `list`, `get`, `openUrl`, `logs`,
139
149
  `followLogs`, and `delete`. Both platforms expose `screenshot`,
150
+ `accessibilityTree`,
140
151
  `startRecording`, `listRecordings`, `getRecording`, `stopRecording`, and
141
152
  `downloadRecording`; iOS additionally supports `uploadVideo` and
142
153
  `uploadMicrophoneAudio`. Inspect the installed types for complete create,
@@ -170,9 +181,10 @@ unavailable, report that requirement instead of guessing a public URL.
170
181
 
171
182
  ## Embed a Session
172
183
 
173
- - Use `RemoteControl` from `@run-cloud/ui` with the signed session URL.
184
+ - Use `RemoteControl` from `@runcloud/ui` with the signed session URL.
174
185
  - Its ref exposes promise-based `interact` and convenience methods matching the
175
- SDK controls. Handle `onInteractionResult` for inspectable acknowledgements.
186
+ SDK controls, plus `accessibilityTree`. Handle `onInteractionResult` for
187
+ inspectable acknowledgements and `onAccessibilityResult` for tree reads.
176
188
  - Add `embed=1` to raw iframe URLs. Add `loadingGuard=1` when interaction should
177
189
  wait for streaming and app readiness.
178
190
  - Raw iframe requests use `run-cloud:interaction`; acknowledgements use
Binary file