@rtrvr-ai/rover 4.0.1 → 4.2.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/index.d.ts CHANGED
@@ -45,7 +45,7 @@ export type RoverInit = {
45
45
  pageConfig?: RoverPageCaptureConfig;
46
46
  allowedDomains?: string[];
47
47
  domainScopeMode?: 'host_only' | 'registrable_domain';
48
- externalNavigationPolicy?: 'open_new_tab_notice' | 'block' | 'allow';
48
+ cloudSandboxEnabled?: boolean;
49
49
  tabPolicy?: {
50
50
  observerByDefault?: boolean;
51
51
  actionLeaseMs?: number;
@@ -55,9 +55,6 @@ export type RoverInit = {
55
55
  actHeuristicThreshold?: number;
56
56
  plannerOnActError?: boolean;
57
57
  };
58
- navigation?: {
59
- crossHostPolicy?: 'open_new_tab' | 'same_tab';
60
- };
61
58
  timing?: {
62
59
  /** Delay (ms) before same-tab navigation executes, allowing state persistence. Default: 80 */
63
60
  navigationDelayMs?: number;
@@ -211,7 +208,7 @@ export type ClientToolDefinition = {
211
208
  annotations?: Record<string, any>;
212
209
  llmCallable?: boolean;
213
210
  };
214
- export type RoverEventName = 'ready' | 'updated' | 'status' | 'run_started' | 'run_state_transition' | 'run_completed' | 'tool_start' | 'tool_result' | 'error' | 'auth_required' | 'navigation_guardrail' | 'mode_change' | 'task_started' | 'task_ended' | 'task_suggested_reset' | 'context_restored' | 'checkpoint_state' | 'checkpoint_error' | 'tab_event_conflict_retry' | 'tab_event_conflict_exhausted' | 'checkpoint_token_missing' | 'open' | 'close';
211
+ export type RoverEventName = 'ready' | 'updated' | 'status' | 'visit_started' | 'visit_ended' | 'run_started' | 'run_state_transition' | 'run_completed' | 'tool_start' | 'tool_result' | 'error' | 'auth_required' | 'navigation_guardrail' | 'mode_change' | 'task_suggested_reset' | 'context_restored' | 'checkpoint_state' | 'checkpoint_error' | 'tab_event_conflict_retry' | 'tab_event_conflict_exhausted' | 'checkpoint_token_missing' | 'open' | 'close';
215
212
  export type RoverEventHandler = (payload?: any) => void;
216
213
  export type RoverPromptContextEntry = {
217
214
  role?: 'model';
@@ -267,7 +264,7 @@ declare function buildPublicRunStartedPayload(msg: any): Record<string, unknown>
267
264
  declare function buildPublicRunLifecyclePayload(msg: any, completionState: ReturnType<typeof normalizeRunCompletionState>): Record<string, unknown>;
268
265
  export declare function getAgentCard(): RoverAgentCard | null;
269
266
  declare function normalizeRunCompletionState(msg: any): {
270
- taskComplete: boolean;
267
+ runComplete: boolean;
271
268
  needsUserInput: boolean;
272
269
  terminalState: 'waiting_input' | 'in_progress' | 'completed' | 'failed';
273
270
  contextResetRecommended: boolean;
@@ -13,7 +13,6 @@ export type RoverOwnerInstallBootConfig = {
13
13
  workerUrl?: string;
14
14
  allowedDomains?: string[];
15
15
  domainScopeMode?: 'host_only' | 'registrable_domain';
16
- externalNavigationPolicy?: 'open_new_tab_notice' | 'block' | 'allow';
17
16
  cloudSandboxEnabled?: boolean;
18
17
  sessionScope?: 'shared_site' | 'tab';
19
18
  openOnInit?: boolean;
@@ -26,9 +25,6 @@ export type RoverOwnerInstallBootConfig = {
26
25
  consume?: boolean;
27
26
  };
28
27
  pageConfig?: RoverPageCaptureConfig | null;
29
- navigation?: {
30
- crossHostPolicy?: 'open_new_tab' | 'same_tab';
31
- };
32
28
  tabPolicy?: {
33
29
  observerByDefault?: boolean;
34
30
  actionLeaseMs?: number;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_AGENT_CARD_PATH, DEFAULT_ROVER_SITE_PATH, createRoverAgentCard, createRoverAgentCardJson, createRoverSiteProfile, createRoverSiteProfileJson, createRoverServiceDescLinkHeader, } from './agentDiscovery.js';
1
+ import { DEFAULT_AGENT_CARD_PATH, DEFAULT_LLMS_PATH, DEFAULT_ROVER_SITE_PATH, createRoverAgentCard, createRoverAgentCardJson, createRoverSiteProfile, createRoverSiteProfileJson, createRoverServiceDescLinkHeader, } from './agentDiscovery.js';
2
2
  const DEFAULT_EMBED_SCRIPT_URL = 'https://rover.rtrvr.ai/embed.js';
3
3
  const DEFAULT_ROVERBOOK_SCRIPT_URL = 'https://rover.rtrvr.ai/roverbook.js';
4
4
  function text(value) {
@@ -135,8 +135,10 @@ function llmsEnabled(input, config) {
135
135
  return input.emitLlmsTxt === true || !!text(config.llmsUrl);
136
136
  }
137
137
  function buildOwnerMarker(card, publishedAgentCardUrl) {
138
+ const runEndpoint = card.extensions?.rover.runEndpoint;
138
139
  return {
139
- task: card.extensions?.rover.taskEndpoint,
140
+ a2w: runEndpoint,
141
+ run: runEndpoint,
140
142
  card: publishedAgentCardUrl,
141
143
  roverSite: card.extensions?.rover.roverSiteUrl,
142
144
  site: card.extensions?.rover.siteUrl,
@@ -167,9 +169,9 @@ function buildDefaultLlmsTxt(card, options) {
167
169
  '',
168
170
  card.description,
169
171
  '',
170
- 'Prefer Rover shortcuts, explicit site tools, and public task flows over raw DOM automation when they match the requested outcome.',
171
- `Primary task endpoint: ${text(card.extensions?.rover.taskEndpoint || card.url)}`,
172
- `Workflow endpoint: ${text(card.extensions?.rover.workflowEndpoint)}`,
172
+ 'Prefer Rover shortcuts, explicit site tools, and A2W runs over raw DOM automation when they match the requested outcome.',
173
+ `Primary A2W run endpoint: ${text(card.extensions?.rover.runEndpoint || card.url)}`,
174
+ `A2W workflow endpoint: ${text(card.extensions?.rover.workflowEndpoint)}`,
173
175
  `Capability card: ${options.agentCardUrl}`,
174
176
  ];
175
177
  const skills = card.skills
@@ -177,7 +179,7 @@ function buildDefaultLlmsTxt(card, options) {
177
179
  id: text(skill.id),
178
180
  name: text(skill.name),
179
181
  description: text(skill.description),
180
- interface: text(skill.preferredInterface || skill.rover?.source || 'task'),
182
+ interface: text(skill.preferredInterface || skill.rover?.source || 'run'),
181
183
  }))
182
184
  .filter(skill => skill.id && skill.name);
183
185
  if (skills.length > 0) {
@@ -258,20 +260,23 @@ function buildRoverBookAttachScript(config, options) {
258
260
  export function createRoverOwnerInstallBundle(input) {
259
261
  const bootConfig = materializeCloudSandboxBootConfig(input.bootConfig);
260
262
  const discoveryConfig = discoveryEnabled(input.discovery) ? input.discovery : null;
263
+ const publishLlmsTxt = llmsEnabled(input, discoveryConfig);
261
264
  const publishedAgentCardUrl = discoveryConfig ? text(discoveryConfig.agentCardUrl) || DEFAULT_AGENT_CARD_PATH : '';
262
265
  const publishedRoverSiteUrl = discoveryConfig ? text(discoveryConfig.roverSiteUrl) || DEFAULT_ROVER_SITE_PATH : '';
263
- const publishLlmsTxt = llmsEnabled(input, discoveryConfig);
264
- const publishedLlmsUrl = discoveryConfig ? text(discoveryConfig.llmsUrl) : '';
266
+ const publishedLlmsUrl = discoveryConfig ? text(discoveryConfig.llmsUrl) || (publishLlmsTxt ? DEFAULT_LLMS_PATH : '') : '';
267
+ const effectiveDiscoveryConfig = discoveryConfig && publishedLlmsUrl && !text(discoveryConfig.llmsUrl)
268
+ ? { ...discoveryConfig, llmsUrl: publishedLlmsUrl }
269
+ : discoveryConfig;
265
270
  const embedScriptUrl = text(input.embedScriptUrl) || DEFAULT_EMBED_SCRIPT_URL;
266
271
  const roverBookEnabled = input.roverBook?.enabled !== false && hasObjectEntries(input.roverBook?.config);
267
272
  const roverBookScriptUrl = roverBookEnabled
268
273
  ? (text(input.roverBook?.scriptUrl) || DEFAULT_ROVERBOOK_SCRIPT_URL)
269
274
  : '';
270
- const agentCard = discoveryConfig ? createRoverAgentCard(discoveryConfig) : undefined;
271
- const agentCardJson = discoveryConfig ? createRoverAgentCardJson(discoveryConfig) : undefined;
272
- const roverSite = discoveryConfig ? createRoverSiteProfile(discoveryConfig) : undefined;
273
- const roverSiteJson = discoveryConfig ? createRoverSiteProfileJson(discoveryConfig) : undefined;
274
- const pageManifestJson = discoveryConfig
275
+ const agentCard = effectiveDiscoveryConfig ? createRoverAgentCard(effectiveDiscoveryConfig) : undefined;
276
+ const agentCardJson = effectiveDiscoveryConfig ? createRoverAgentCardJson(effectiveDiscoveryConfig) : undefined;
277
+ const roverSite = effectiveDiscoveryConfig ? createRoverSiteProfile(effectiveDiscoveryConfig) : undefined;
278
+ const roverSiteJson = effectiveDiscoveryConfig ? createRoverSiteProfileJson(effectiveDiscoveryConfig) : undefined;
279
+ const pageManifestJson = effectiveDiscoveryConfig
275
280
  ? JSON.stringify(agentCard?.extensions?.rover.currentPage || null, null, 2)
276
281
  : undefined;
277
282
  const marker = agentCard && publishedAgentCardUrl
@@ -7,8 +7,24 @@ export type RoverPreviewBootstrapVoiceConfig = {
7
7
  language?: string;
8
8
  autoStopMs?: number;
9
9
  };
10
+ export type RoverPreviewBootstrapExperienceConfig = {
11
+ audio?: {
12
+ narration?: {
13
+ enabled?: boolean;
14
+ defaultMode?: 'guided' | 'always' | 'off';
15
+ rate?: number;
16
+ language?: string;
17
+ voicePreference?: 'auto' | 'system' | 'natural';
18
+ };
19
+ };
20
+ motion?: {
21
+ actionSpotlight?: boolean;
22
+ actionSpotlightColor?: string;
23
+ };
24
+ };
10
25
  export type RoverPreviewBootstrapUiConfig = {
11
26
  voice?: RoverPreviewBootstrapVoiceConfig;
27
+ experience?: RoverPreviewBootstrapExperienceConfig;
12
28
  };
13
29
  export type RoverPreviewBootstrapConfig = {
14
30
  scriptUrl?: string;
@@ -21,11 +37,14 @@ export type RoverPreviewBootstrapConfig = {
21
37
  workerUrl?: string;
22
38
  allowedDomains?: string[];
23
39
  domainScopeMode?: 'host_only' | 'registrable_domain';
24
- externalNavigationPolicy?: 'open_new_tab_notice' | 'block' | 'allow';
40
+ cloudSandboxEnabled?: boolean;
25
41
  sessionScope?: 'shared_site' | 'tab';
26
42
  openOnInit?: boolean;
27
43
  mode?: 'safe' | 'full';
28
44
  allowActions?: boolean;
45
+ pageConfig?: {
46
+ disableAutoScroll?: boolean;
47
+ };
29
48
  ui?: RoverPreviewBootstrapUiConfig;
30
49
  attachLaunch?: RoverPreviewAttachLaunch;
31
50
  };
@@ -2,6 +2,8 @@ const DEFAULT_EMBED_SCRIPT_URL = 'https://rover.rtrvr.ai/embed.js';
2
2
  const DEFAULT_AGENT_BASE = 'https://agent.rtrvr.ai';
3
3
  const VOICE_AUTO_STOP_MIN_MS = 800;
4
4
  const VOICE_AUTO_STOP_MAX_MS = 5000;
5
+ const NARRATION_RATE_MIN = 0.85;
6
+ const NARRATION_RATE_MAX = 1.15;
5
7
  function toStringValue(value) {
6
8
  return String(value || '').trim();
7
9
  }
@@ -41,6 +43,13 @@ function parseIntegerAttr(value) {
41
43
  const parsed = Number(toStringValue(value));
42
44
  return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined;
43
45
  }
46
+ function normalizeHexColor(value) {
47
+ const raw = toStringValue(value);
48
+ if (!raw)
49
+ return undefined;
50
+ const match = raw.match(/^#?([0-9a-fA-F]{6})$/);
51
+ return match ? `#${match[1].toUpperCase()}` : undefined;
52
+ }
44
53
  function normalizeVoiceConfig(value) {
45
54
  if (!value || typeof value !== 'object')
46
55
  return undefined;
@@ -56,6 +65,53 @@ function normalizeVoiceConfig(value) {
56
65
  }
57
66
  return Object.keys(voice).length ? voice : undefined;
58
67
  }
68
+ function normalizeExperienceConfig(value) {
69
+ if (!value || typeof value !== 'object')
70
+ return undefined;
71
+ const experience = {};
72
+ if (value.audio && typeof value.audio === 'object') {
73
+ const narrationInput = value.audio.narration && typeof value.audio.narration === 'object'
74
+ ? value.audio.narration
75
+ : undefined;
76
+ if (narrationInput) {
77
+ const narration = {};
78
+ if (typeof narrationInput.enabled === 'boolean')
79
+ narration.enabled = narrationInput.enabled;
80
+ if (narrationInput.defaultMode === 'guided' ||
81
+ narrationInput.defaultMode === 'always' ||
82
+ narrationInput.defaultMode === 'off') {
83
+ narration.defaultMode = narrationInput.defaultMode;
84
+ }
85
+ const rate = Number(narrationInput.rate);
86
+ if (Number.isFinite(rate)) {
87
+ narration.rate = Math.max(NARRATION_RATE_MIN, Math.min(NARRATION_RATE_MAX, rate));
88
+ }
89
+ const language = toStringValue(narrationInput.language).replace(/[^a-zA-Z0-9-]/g, '').slice(0, 24);
90
+ if (language)
91
+ narration.language = language;
92
+ if (narrationInput.voicePreference === 'auto' ||
93
+ narrationInput.voicePreference === 'system' ||
94
+ narrationInput.voicePreference === 'natural') {
95
+ narration.voicePreference = narrationInput.voicePreference;
96
+ }
97
+ if (Object.keys(narration).length)
98
+ experience.audio = { narration };
99
+ }
100
+ }
101
+ if (value.motion && typeof value.motion === 'object') {
102
+ const motion = {};
103
+ if (typeof value.motion.actionSpotlight === 'boolean') {
104
+ motion.actionSpotlight = value.motion.actionSpotlight;
105
+ }
106
+ const actionSpotlightColor = normalizeHexColor(value.motion.actionSpotlightColor);
107
+ if (actionSpotlightColor) {
108
+ motion.actionSpotlightColor = actionSpotlightColor;
109
+ }
110
+ if (Object.keys(motion).length)
111
+ experience.motion = motion;
112
+ }
113
+ return Object.keys(experience).length ? experience : undefined;
114
+ }
59
115
  function normalizeUiConfig(value) {
60
116
  if (!value || typeof value !== 'object')
61
117
  return undefined;
@@ -63,6 +119,9 @@ function normalizeUiConfig(value) {
63
119
  const voice = normalizeVoiceConfig(value.voice);
64
120
  if (voice)
65
121
  ui.voice = voice;
122
+ const experience = normalizeExperienceConfig(value.experience);
123
+ if (experience)
124
+ ui.experience = experience;
66
125
  return Object.keys(ui).length ? ui : undefined;
67
126
  }
68
127
  function normalizeBootstrapConfig(config) {
@@ -98,8 +157,8 @@ function buildBootstrapPayload(config) {
98
157
  payload.allowedDomains = normalized.allowedDomains;
99
158
  if (normalized.domainScopeMode)
100
159
  payload.domainScopeMode = normalized.domainScopeMode;
101
- if (normalized.externalNavigationPolicy)
102
- payload.externalNavigationPolicy = normalized.externalNavigationPolicy;
160
+ if (typeof normalized.cloudSandboxEnabled === 'boolean')
161
+ payload.cloudSandboxEnabled = normalized.cloudSandboxEnabled;
103
162
  if (normalized.sessionScope)
104
163
  payload.sessionScope = normalized.sessionScope;
105
164
  if (typeof normalized.openOnInit === 'boolean')
@@ -108,8 +167,11 @@ function buildBootstrapPayload(config) {
108
167
  payload.mode = normalized.mode;
109
168
  if (typeof normalized.allowActions === 'boolean')
110
169
  payload.allowActions = normalized.allowActions;
111
- if (normalized.ui?.voice)
112
- payload.ui = { voice: normalized.ui.voice };
170
+ if (typeof normalized.pageConfig?.disableAutoScroll === 'boolean') {
171
+ payload.pageConfig = { disableAutoScroll: normalized.pageConfig.disableAutoScroll };
172
+ }
173
+ if (normalized.ui)
174
+ payload.ui = normalized.ui;
113
175
  return payload;
114
176
  }
115
177
  function buildQueueStub() {
@@ -182,8 +244,8 @@ export function createRoverScriptTagSnippet(config) {
182
244
  attrs.push(`data-allowed-domains="${escapeHtmlAttr(normalized.allowedDomains.join(','))}"`);
183
245
  if (normalized.domainScopeMode)
184
246
  attrs.push(`data-domain-scope-mode="${escapeHtmlAttr(normalized.domainScopeMode)}"`);
185
- if (normalized.externalNavigationPolicy)
186
- attrs.push(`data-external-navigation-policy="${escapeHtmlAttr(normalized.externalNavigationPolicy)}"`);
247
+ if (typeof normalized.cloudSandboxEnabled === 'boolean')
248
+ attrs.push(`data-cloud-sandbox-enabled="${escapeHtmlAttr(String(normalized.cloudSandboxEnabled))}"`);
187
249
  if (normalized.sessionScope)
188
250
  attrs.push(`data-session-scope="${escapeHtmlAttr(normalized.sessionScope)}"`);
189
251
  if (typeof normalized.openOnInit === 'boolean')
@@ -192,14 +254,34 @@ export function createRoverScriptTagSnippet(config) {
192
254
  attrs.push(`data-mode="${escapeHtmlAttr(normalized.mode)}"`);
193
255
  if (typeof normalized.allowActions === 'boolean')
194
256
  attrs.push(`data-allow-actions="${escapeHtmlAttr(String(normalized.allowActions))}"`);
257
+ if (typeof normalized.pageConfig?.disableAutoScroll === 'boolean') {
258
+ attrs.push(`data-disable-auto-scroll="${escapeHtmlAttr(String(normalized.pageConfig.disableAutoScroll))}"`);
259
+ }
195
260
  if (typeof normalized.ui?.voice?.enabled === 'boolean')
196
261
  attrs.push(`data-voice-enabled="${escapeHtmlAttr(String(normalized.ui.voice.enabled))}"`);
197
262
  if (normalized.ui?.voice?.language)
198
263
  attrs.push(`data-voice-language="${escapeHtmlAttr(normalized.ui.voice.language)}"`);
199
264
  if (typeof normalized.ui?.voice?.autoStopMs === 'number')
200
265
  attrs.push(`data-voice-auto-stop-ms="${escapeHtmlAttr(String(normalized.ui.voice.autoStopMs))}"`);
201
- const taskEndpoint = `${toStringValue(normalized.apiBase) || DEFAULT_AGENT_BASE}/v1/tasks`;
202
- const markerJson = escapeScriptJson(JSON.stringify({ task: taskEndpoint }));
266
+ const narration = normalized.ui?.experience?.audio?.narration;
267
+ if (typeof narration?.enabled === 'boolean')
268
+ attrs.push(`data-narration-enabled="${escapeHtmlAttr(String(narration.enabled))}"`);
269
+ if (narration?.defaultMode)
270
+ attrs.push(`data-narration-default-mode="${escapeHtmlAttr(narration.defaultMode)}"`);
271
+ if (typeof narration?.rate === 'number')
272
+ attrs.push(`data-narration-rate="${escapeHtmlAttr(String(narration.rate))}"`);
273
+ if (narration?.language)
274
+ attrs.push(`data-narration-language="${escapeHtmlAttr(narration.language)}"`);
275
+ if (narration?.voicePreference)
276
+ attrs.push(`data-narration-voice-preference="${escapeHtmlAttr(narration.voicePreference)}"`);
277
+ if (typeof normalized.ui?.experience?.motion?.actionSpotlight === 'boolean') {
278
+ attrs.push(`data-action-spotlight="${escapeHtmlAttr(String(normalized.ui.experience.motion.actionSpotlight))}"`);
279
+ }
280
+ if (normalized.ui?.experience?.motion?.actionSpotlightColor) {
281
+ attrs.push(`data-action-spotlight-color="${escapeHtmlAttr(normalized.ui.experience.motion.actionSpotlightColor)}"`);
282
+ }
283
+ const runEndpoint = `${toStringValue(normalized.apiBase) || DEFAULT_AGENT_BASE}/v1/a2w/runs`;
284
+ const markerJson = escapeScriptJson(JSON.stringify({ a2w: runEndpoint, run: runEndpoint }));
203
285
  return [
204
286
  `<script type="application/agent+json" data-rover-agent-discovery="marker">${markerJson}</script>`,
205
287
  '<link rel="service-desc" href="/.well-known/agent-card.json" type="application/json" data-rover-agent-discovery="service-desc" />',
@@ -239,10 +321,9 @@ export function readRoverScriptDataAttributes(scriptEl) {
239
321
  if (domainScopeMode === 'host_only' || domainScopeMode === 'registrable_domain') {
240
322
  config.domainScopeMode = domainScopeMode;
241
323
  }
242
- const externalNavigationPolicy = toStringValue(scriptEl.getAttribute('data-external-navigation-policy'));
243
- if (externalNavigationPolicy === 'open_new_tab_notice' || externalNavigationPolicy === 'block' || externalNavigationPolicy === 'allow') {
244
- config.externalNavigationPolicy = externalNavigationPolicy;
245
- }
324
+ const cloudSandboxEnabled = parseBooleanAttr(scriptEl.getAttribute('data-cloud-sandbox-enabled'));
325
+ if (typeof cloudSandboxEnabled === 'boolean')
326
+ config.cloudSandboxEnabled = cloudSandboxEnabled;
246
327
  const sessionScope = toStringValue(scriptEl.getAttribute('data-session-scope'));
247
328
  if (sessionScope === 'shared_site' || sessionScope === 'tab') {
248
329
  config.sessionScope = sessionScope;
@@ -257,16 +338,50 @@ export function readRoverScriptDataAttributes(scriptEl) {
257
338
  const allowActions = parseBooleanAttr(scriptEl.getAttribute('data-allow-actions'));
258
339
  if (typeof allowActions === 'boolean')
259
340
  config.allowActions = allowActions;
341
+ const disableAutoScroll = parseBooleanAttr(scriptEl.getAttribute('data-disable-auto-scroll'));
342
+ if (typeof disableAutoScroll === 'boolean')
343
+ config.pageConfig = { disableAutoScroll };
260
344
  const voiceEnabled = parseBooleanAttr(scriptEl.getAttribute('data-voice-enabled'));
261
345
  const voiceLanguage = toStringValue(scriptEl.getAttribute('data-voice-language'));
262
346
  const voiceAutoStopMs = parseIntegerAttr(scriptEl.getAttribute('data-voice-auto-stop-ms'));
263
- if (typeof voiceEnabled === 'boolean' || voiceLanguage || typeof voiceAutoStopMs === 'number') {
347
+ const narrationEnabled = parseBooleanAttr(scriptEl.getAttribute('data-narration-enabled'));
348
+ const narrationDefaultMode = toStringValue(scriptEl.getAttribute('data-narration-default-mode'));
349
+ const narrationRate = Number(toStringValue(scriptEl.getAttribute('data-narration-rate')));
350
+ const narrationLanguage = toStringValue(scriptEl.getAttribute('data-narration-language'));
351
+ const narrationVoicePreference = toStringValue(scriptEl.getAttribute('data-narration-voice-preference'));
352
+ const actionSpotlight = parseBooleanAttr(scriptEl.getAttribute('data-action-spotlight'));
353
+ const actionSpotlightColor = normalizeHexColor(scriptEl.getAttribute('data-action-spotlight-color'));
354
+ const voice = normalizeVoiceConfig({
355
+ ...(typeof voiceEnabled === 'boolean' ? { enabled: voiceEnabled } : {}),
356
+ ...(voiceLanguage ? { language: voiceLanguage } : {}),
357
+ ...(typeof voiceAutoStopMs === 'number' ? { autoStopMs: voiceAutoStopMs } : {}),
358
+ });
359
+ const experience = normalizeExperienceConfig({
360
+ ...(typeof narrationEnabled === 'boolean' || narrationDefaultMode || Number.isFinite(narrationRate) || narrationLanguage || narrationVoicePreference
361
+ ? {
362
+ audio: {
363
+ narration: {
364
+ ...(typeof narrationEnabled === 'boolean' ? { enabled: narrationEnabled } : {}),
365
+ ...(narrationDefaultMode === 'guided' || narrationDefaultMode === 'always' || narrationDefaultMode === 'off'
366
+ ? { defaultMode: narrationDefaultMode }
367
+ : {}),
368
+ ...(Number.isFinite(narrationRate) ? { rate: narrationRate } : {}),
369
+ ...(narrationLanguage ? { language: narrationLanguage } : {}),
370
+ ...(narrationVoicePreference === 'auto' || narrationVoicePreference === 'system' || narrationVoicePreference === 'natural'
371
+ ? { voicePreference: narrationVoicePreference }
372
+ : {}),
373
+ },
374
+ },
375
+ }
376
+ : {}),
377
+ ...(typeof actionSpotlight === 'boolean' || actionSpotlightColor
378
+ ? { motion: { ...(typeof actionSpotlight === 'boolean' ? { actionSpotlight } : {}), ...(actionSpotlightColor ? { actionSpotlightColor } : {}) } }
379
+ : {}),
380
+ });
381
+ if (voice || experience) {
264
382
  config.ui = {
265
- voice: {
266
- ...(typeof voiceEnabled === 'boolean' ? { enabled: voiceEnabled } : {}),
267
- ...(voiceLanguage ? { language: voiceLanguage } : {}),
268
- ...(typeof voiceAutoStopMs === 'number' ? { autoStopMs: voiceAutoStopMs } : {}),
269
- },
383
+ ...(voice ? { voice } : {}),
384
+ ...(experience ? { experience } : {}),
270
385
  };
271
386
  }
272
387
  return config;