@wix/mcp 1.0.83 → 1.0.85

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.
@@ -80,6 +80,16 @@ export function collectDecks(projectAssets, deckIds) {
80
80
  }
81
81
  return decks;
82
82
  }
83
+ /**
84
+ * A reply that ends by asking the user something (a plan to approve, a
85
+ * choice to make) is not a delivery even when it links media — an animation
86
+ * plan links the source image it will animate. Asset-id markers ("Asset
87
+ * ready for … (id)") only appear on real deliveries; media URLs alone need
88
+ * this check.
89
+ */
90
+ function isQuestionReply(text) {
91
+ return /\?\s*$/.test(text.trim());
92
+ }
83
93
  // Image/video replies often carry no asset-id marker — just the media URL in
84
94
  // the text: a wixstatic file, a wixmp render (img-wixmp-*/wixmp-* hosts), or a
85
95
  // links.wixel.com short link that redirects to one. Those links are the
@@ -223,12 +233,87 @@ const QUESTION_TOOL_META = {
223
233
  visibility: ['app', 'model']
224
234
  }
225
235
  };
236
+ const MAX_LIVE_CARD_HANDLES = 500;
237
+ // A card that never reported done (unmounted, tab closed, timed out) must not
238
+ // lock the agent out of the handle forever.
239
+ const LIVE_CARD_TTL_MS = 30 * 60 * 1000;
240
+ export function createLiveCardRegistry() {
241
+ const records = new Map();
242
+ const get = (handle) => records.get(handle);
243
+ const forHandle = (handle) => {
244
+ let record = records.get(handle);
245
+ if (!record) {
246
+ if (records.size >= MAX_LIVE_CARD_HANDLES) {
247
+ const oldest = records.keys().next().value;
248
+ if (oldest !== undefined)
249
+ records.delete(oldest);
250
+ }
251
+ record = {
252
+ turns: new Map(),
253
+ finishedTurns: new Set(),
254
+ shownAssets: new Set(),
255
+ lastSentTurn: null
256
+ };
257
+ records.set(handle, record);
258
+ }
259
+ return record;
260
+ };
261
+ /**
262
+ * Whether the turn the agent last sent is being followed by a live card
263
+ * that has not finished. Only the card's own polls mark a turn finished,
264
+ * so when the card is gone this also consults the inbox: a terminal event
265
+ * for the turn ends the card's claim, as does the TTL.
266
+ */
267
+ const isActive = async (handle, params) => {
268
+ const record = records.get(handle);
269
+ const turnId = record?.lastSentTurn;
270
+ if (!record || !turnId)
271
+ return false;
272
+ const renderedAt = record.turns.get(turnId);
273
+ if (renderedAt === undefined || record.finishedTurns.has(turnId)) {
274
+ return false;
275
+ }
276
+ if (Date.now() - renderedAt > LIVE_CARD_TTL_MS) {
277
+ record.finishedTurns.add(turnId);
278
+ return false;
279
+ }
280
+ try {
281
+ if (await hasTerminalEvent({ ...params, handle, turnId })) {
282
+ record.finishedTurns.add(turnId);
283
+ return false;
284
+ }
285
+ }
286
+ catch (error) {
287
+ // Can't tell — keep the card's claim rather than let a second card in.
288
+ console.warn('[Wixel] live card terminal check failed:', error.message);
289
+ }
290
+ return true;
291
+ };
292
+ return { get, forHandle, isActive };
293
+ }
294
+ // Replays the inbox from the start (explicit cursor, so the agent's remembered
295
+ // cursor never moves) and reports whether the turn has its reply/failed event.
296
+ async function hasTerminalEvent(params) {
297
+ const inbox = await listWixelSessionEvents({
298
+ authHeaders: params.authHeaders,
299
+ conductorOverride: params.conductorOverride,
300
+ handle: params.handle,
301
+ metaSiteId: params.metaSiteId,
302
+ since: '0'
303
+ });
304
+ if (inbox.error) {
305
+ throw new Error(`${inbox.error} (${inbox.code ?? 'no code'})`);
306
+ }
307
+ return (inbox.events ?? []).some((event) => event.turnId === params.turnId &&
308
+ (event.type === 'reply' || event.type === 'failed'));
309
+ }
226
310
  /** McpAppsBiProvider builds the widget's BI logger from
227
311
  * widgetPayload.extras.biContext; without it every widget event is dropped. */
228
312
  function widgetResultMeta(server) {
229
313
  return { widgetPayload: { extras: server.getWidgetExtras() } };
230
314
  }
231
315
  export function addGetWixelPendingQuestionTool(server, authStrategy, options = {}) {
316
+ const liveCards = options.liveCards ?? createLiveCardRegistry();
232
317
  const { getSiteAuthHeaders } = authStrategy;
233
318
  // The wixel widget resource used by widget-bound tools (registered here
234
319
  // since the session tools are now the conversation surface).
@@ -288,6 +373,32 @@ Call this after a reply event whenever the reply asks the user something. In wid
288
373
  sessionId: args.handle,
289
374
  conductorOverride: options.conductorOverride
290
375
  });
376
+ // The live card renders this question inline; a second, standalone
377
+ // question widget would put it in the chat twice.
378
+ if (pending &&
379
+ options.supportsWidgets?.() === true &&
380
+ (await liveCards.isActive(args.handle, {
381
+ authHeaders,
382
+ conductorOverride: options.conductorOverride,
383
+ metaSiteId: args.metaSiteId
384
+ }))) {
385
+ console.log('[WixelResponseDump]', JSON.stringify({
386
+ tool: 'get_wixel_pending_question',
387
+ args,
388
+ suppressed: pending.toolCallId
389
+ }));
390
+ return {
391
+ isError: false,
392
+ content: [
393
+ {
394
+ type: 'text',
395
+ text: `The live result card already shows this question to the user and sends the answer to Wixel itself.
396
+ <agent_instruction>Do NOT present the question and do NOT poll list_wixel_events — the card follows the turn. Do not tell the user to answer either — the card is self-explanatory. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
397
+ }
398
+ ],
399
+ structuredContent: { success: true, pendingQuestion: null }
400
+ };
401
+ }
291
402
  // A caller-supplied binding that no longer matches the live pending
292
403
  // question means that question was already handled — report "nothing
293
404
  // pending" instead of a different question.
@@ -450,6 +561,7 @@ Without arguments Wixel provisions a new project and session. Pass metaSiteId to
450
561
  });
451
562
  }
452
563
  export function addSendWixelMessageTool(server, authStrategy, options = {}) {
564
+ const liveCards = options.liveCards ?? createLiveCardRegistry();
453
565
  const { getSiteAuthHeaders } = authStrategy;
454
566
  registerAppTool(server, 'send_wixel_message', {
455
567
  title: 'Send Wixel message',
@@ -483,14 +595,12 @@ Send the user's request as THEY phrased it. Do NOT add creative direction they d
483
595
  });
484
596
  const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
485
597
  try {
486
- // The external-session policy answers ask_user itself, so the agent
487
- // has no reason to ask — but the structured payload is still persisted
488
- // and get_wixel_pending_question renders it as a widget. Say so, or
489
- // the outline and style the user was about to choose get decided for
490
- // them. Mirrors the hint the widget appends to an answer.
491
- const text = options.supportsWidgets?.()
492
- ? `${args.text}\n(This client renders structured ask_user questions as interactive widgets. If the request leaves the outline, style or palette open, call ask_user and WAIT for a real answer before generating — do not fill the gaps yourself, and do not proceed on a default: once the asset exists the user can no longer choose.)`
493
- : args.text;
598
+ // The user's text goes through as typed — nothing is appended. An
599
+ // earlier hint here asked Wixel to stop and ask about style/palette
600
+ // whenever a request left them open, which is why MCP users got
601
+ // questions the Wixel app never asks. Wixel's own flows (a deck's
602
+ // outline and style review) still ask, and the live card renders them.
603
+ const text = args.text;
494
604
  const result = await sendWixelSessionMessage({
495
605
  authHeaders,
496
606
  conductorOverride: options.conductorOverride,
@@ -520,22 +630,35 @@ Send the user's request as THEY phrased it. Do NOT add creative direction they d
520
630
  }
521
631
  };
522
632
  }
633
+ if (result.turnId) {
634
+ liveCards.forHandle(args.handle).lastSentTurn = result.turnId;
635
+ }
636
+ // Widget clients get a live result card that follows the turn itself
637
+ // (progress, questions, final asset), so the agent renders it once
638
+ // and stops — no polling loop of tool calls in the conversation.
639
+ const instruction = options.supportsWidgets?.()
640
+ ? `<agent_instruction>Now call get_wixel_asset_result with handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", turnId: "${result.turnId}", prompt: <the user's request as they phrased it>. It renders a live result card that shows progress, asks the user any question Wixel has, and displays the final asset. Then end your turn — do NOT poll list_wixel_events and do NOT call get_wixel_pending_question, and do NOT write a message after the card (it explains itself).</agent_instruction>`
641
+ : `<agent_instruction>Poll list_wixel_events (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25, advancing the returned cursor) until an event with turnId "${result.turnId}" has type "reply" or "failed". Turns can take minutes for generation tasks — keep polling.
642
+ After a reply that asks the user something (review an outline, pick a style, confirm), call get_wixel_pending_question (same handle/metaSiteId) and follow its response instruction.</agent_instruction>`;
643
+ const structuredContent = {
644
+ success: true,
645
+ sessionId: result.sessionId,
646
+ turnId: result.turnId,
647
+ transactionId: result.transactionId ?? null
648
+ };
523
649
  return {
524
650
  isError: false,
525
651
  content: [
526
652
  {
527
653
  type: 'text',
528
654
  text: `Turn accepted — turnId: ${result.turnId}.
529
- <agent_instruction>Poll list_wixel_events (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25, advancing the returned cursor) until an event with turnId "${result.turnId}" has type "reply" or "failed". Turns can take minutes for generation tasks — keep polling.
530
- After a reply that asks the user something (review an outline, pick a style, confirm), call get_wixel_pending_question (same handle/metaSiteId) and follow its response instruction.</agent_instruction>`
531
- }
655
+ ${instruction}`
656
+ },
657
+ // The widgets send answers through this tool and need the turnId
658
+ // back; hosts that drop structuredContent get it from this item.
659
+ { type: 'text', text: JSON.stringify(structuredContent) }
532
660
  ],
533
- structuredContent: {
534
- success: true,
535
- sessionId: result.sessionId,
536
- turnId: result.turnId,
537
- transactionId: result.transactionId ?? null
538
- }
661
+ structuredContent
539
662
  };
540
663
  }
541
664
  catch (error) {
@@ -551,6 +674,7 @@ After a reply that asks the user something (review an outline, pick a style, con
551
674
  });
552
675
  }
553
676
  export function addListWixelEventsTool(server, authStrategy, options = {}) {
677
+ const liveCards = options.liveCards ?? createLiveCardRegistry();
554
678
  const { getSiteAuthHeaders } = authStrategy;
555
679
  // Models routinely forget to thread the cursor through poll loops, which
556
680
  // makes them re-read old replies and misjudge the session state. Remember
@@ -603,6 +727,33 @@ Event types: "reply" (the agent's answer), "failed" (the turn failed), and "user
603
727
  since,
604
728
  historyMode: args.historyMode
605
729
  });
730
+ // A live result card is following the last turn: the agent has
731
+ // nothing to do here, and text instructions alone don't stop its
732
+ // polling loop — so refuse the call (no fetch, cursor untouched).
733
+ // The tool works again once the turn has ended (the card saw it, or
734
+ // the inbox says so when the card is gone).
735
+ if (options.supportsWidgets?.() === true &&
736
+ (await liveCards.isActive(args.handle, {
737
+ authHeaders,
738
+ conductorOverride: options.conductorOverride,
739
+ metaSiteId: args.metaSiteId
740
+ }))) {
741
+ console.log('[WixelResponseDump]', JSON.stringify({ tool: 'list_wixel_events', args, refused: true }));
742
+ return {
743
+ isError: true,
744
+ content: [
745
+ {
746
+ type: 'text',
747
+ text: `Not needed: the live result card is following this generation and will display progress, any question, and the final result by itself. This is not a failure — there is nothing to wait for here. Do NOT call list_wixel_events, get_wixel_pending_question or get_wixel_asset_result again for this generation. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.`
748
+ }
749
+ ],
750
+ structuredContent: {
751
+ success: false,
752
+ liveCard: true,
753
+ error: 'live result card active'
754
+ }
755
+ };
756
+ }
606
757
  let result = await fetchEvents();
607
758
  // Long-poll: hold the request while the inbox is empty so callers can
608
759
  // wait on slow turns (generation, widget answers) without hammering.
@@ -659,9 +810,30 @@ Event types: "reply" (the agent's answer), "failed" (the turn failed), and "user
659
810
  .filter((event) => event.type === 'reply' && event.text)
660
811
  .map((event) => event.text);
661
812
  deliveredAssetIds = replyTexts.flatMap(extractAssetIds);
662
- deliveredMediaUrls = replyTexts.flatMap(extractMediaUrls);
813
+ // Same rule as the live card: a reply that asks the user something
814
+ // (e.g. an animation plan linking its source image) delivers
815
+ // nothing by its media URLs alone.
816
+ deliveredMediaUrls = replyTexts
817
+ .filter((text) => !isQuestionReply(text))
818
+ .flatMap(extractMediaUrls);
663
819
  if (deliveredAssetIds.length || deliveredMediaUrls.length) {
664
820
  deliveredAssets = true;
821
+ // Delivered by a turn a live card follows: the card shows them,
822
+ // so a get_wixel_asset_result for these ids is a duplicate. Only
823
+ // that turn's ids — a batch may also carry a turn no card follows.
824
+ const record = liveCards.get(args.handle);
825
+ if (record) {
826
+ events
827
+ .filter((event) => event.type === 'reply' &&
828
+ !!event.text &&
829
+ !!event.turnId &&
830
+ record.turns.has(event.turnId))
831
+ .flatMap((event) => [
832
+ ...extractAssetIds(event.text).slice(0, MAX_RESULT_ITEMS),
833
+ ...extractMediaUrls(event.text).slice(0, MAX_RESULT_ITEMS)
834
+ ])
835
+ .forEach((item) => record.shownAssets.add(item));
836
+ }
665
837
  try {
666
838
  const projectId = await getWixelSessionProjectId({
667
839
  authHeaders,
@@ -821,12 +993,380 @@ export function addGetWixelStylePresetsTool(server, authStrategy, options = {})
821
993
  }
822
994
  });
823
995
  }
996
+ // Widget-internal (not exposed to the model). The result card renders the
997
+ // moment the request is sent and drives the rest of the flow itself: it polls
998
+ // this for the answer's turn and gets back "generating", a "question" to show
999
+ // inline, the "done" assets, or "failed". The inbox is replayed from cursor
1000
+ // "0" with an explicit cursor, so the cursor list_wixel_events remembers per
1001
+ // handle for the agent is never advanced. "Done" is the reply/failed event of
1002
+ // the turn — the same signal the agent would have waited on; the entries log
1003
+ // only decides whether the turn is parked on a question, because the agent
1004
+ // emits text mid-turn and that log cannot tell it from the final reply.
1005
+ // `ui.visibility` is what MCP Apps hosts read; ChatGPT reads
1006
+ // `openai/visibility` instead, so both are set or its model would see the tool.
1007
+ const WIDGET_ONLY_TOOL_META = {
1008
+ ui: { visibility: ['app'] },
1009
+ 'openai/visibility': 'private'
1010
+ };
1011
+ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1012
+ const liveCards = options.liveCards ?? createLiveCardRegistry();
1013
+ const { getSiteAuthHeaders } = authStrategy;
1014
+ registerAppTool(server, 'get_wixel_turn_result', {
1015
+ title: 'Get Wixel turn result',
1016
+ description: 'Widget-internal: the live result card polls this for the outcome of a turn (generating, a question to answer, the delivered assets, or a failure). Not needed for normal conversation flows.',
1017
+ inputSchema: {
1018
+ handle: z.string().describe('Session handle from start_wixel_session'),
1019
+ metaSiteId: z
1020
+ .string()
1021
+ .describe('Meta site ID from start_wixel_session'),
1022
+ turnId: z
1023
+ .string()
1024
+ .describe('The turn to report on (returned by send_wixel_message)'),
1025
+ live: z
1026
+ .boolean()
1027
+ .optional()
1028
+ .describe('Set by the live result card: marks the turn as followed by the card, so the agent is told to stand back for it.')
1029
+ },
1030
+ annotations: {
1031
+ readOnlyHint: true,
1032
+ destructiveHint: false,
1033
+ openWorldHint: false
1034
+ },
1035
+ _meta: WIDGET_ONLY_TOOL_META
1036
+ }, async (args, extra) => {
1037
+ setBi(extra, {
1038
+ siteId: args.metaSiteId,
1039
+ conversationId: args.handle
1040
+ });
1041
+ // A turn the live card follows — including the turns its inline
1042
+ // answers open, which get_wixel_asset_result never saw.
1043
+ if (args.live) {
1044
+ const record = liveCards.forHandle(args.handle);
1045
+ if (!record.turns.has(args.turnId)) {
1046
+ record.turns.set(args.turnId, Date.now());
1047
+ }
1048
+ }
1049
+ const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
1050
+ const base = {
1051
+ authHeaders,
1052
+ conductorOverride: options.conductorOverride
1053
+ };
1054
+ try {
1055
+ const inbox = await listWixelSessionEvents({
1056
+ ...base,
1057
+ handle: args.handle,
1058
+ metaSiteId: args.metaSiteId,
1059
+ since: '0'
1060
+ });
1061
+ if (inbox.error) {
1062
+ throw new Error(`Failed to replay events: ${inbox.error} (${inbox.code ?? 'no code'})`);
1063
+ }
1064
+ const terminal = (inbox.events ?? []).find((event) => event.turnId === args.turnId &&
1065
+ (event.type === 'reply' || event.type === 'failed'));
1066
+ let status = 'generating';
1067
+ let assets = [];
1068
+ let editorUrl = null;
1069
+ let pendingQuestion = null;
1070
+ const replyText = terminal?.text ?? null;
1071
+ if (terminal?.type === 'failed') {
1072
+ status = 'failed';
1073
+ }
1074
+ else if (terminal) {
1075
+ // Capped like the resolver's input, so what gets recorded as
1076
+ // shown below is exactly what was considered for rendering.
1077
+ const assetIds = extractAssetIds(terminal.text ?? '').slice(0, MAX_RESULT_ITEMS);
1078
+ const mediaUrls = extractMediaUrls(terminal.text ?? '').slice(0, MAX_RESULT_ITEMS);
1079
+ // Media URLs alone count only when the reply is not a question:
1080
+ // an animation plan links the source image and asks for approval.
1081
+ const delivers = assetIds.length > 0 ||
1082
+ (mediaUrls.length > 0 && !isQuestionReply(terminal.text ?? ''));
1083
+ if (delivers) {
1084
+ status = 'done';
1085
+ // Only the live card displays what it resolves. The standalone
1086
+ // question widget polls for the status alone: resolving for it
1087
+ // would be wasted work, and marking the ids as shown would make
1088
+ // the agent's own get_wixel_asset_result a "duplicate".
1089
+ if (args.live) {
1090
+ ({ assets, editorUrl } = await resolveWixelAssetResults({
1091
+ ...base,
1092
+ handle: args.handle,
1093
+ metaSiteId: args.metaSiteId,
1094
+ assetIds,
1095
+ mediaUrls
1096
+ }));
1097
+ // Only what the card will actually show counts as shown. A
1098
+ // resolution that came back empty (transient lookup failure)
1099
+ // leaves the result for the agent's get_wixel_asset_result.
1100
+ if (assets.length) {
1101
+ const record = liveCards.forHandle(args.handle);
1102
+ [...assetIds, ...mediaUrls].forEach((item) => record.shownAssets.add(item));
1103
+ }
1104
+ }
1105
+ }
1106
+ else {
1107
+ // A reply without a delivery is either a question restated as
1108
+ // text (its structured payload is still pending) or a plain
1109
+ // message — the card shows it with a reply box.
1110
+ pendingQuestion = await getWixelPendingQuestion({
1111
+ ...base,
1112
+ sessionId: args.handle
1113
+ });
1114
+ status = pendingQuestion ? 'question' : 'done';
1115
+ }
1116
+ }
1117
+ else {
1118
+ // No terminal event yet: parked on a question (custom_widget asks
1119
+ // park the turn without a reply event) or still generating.
1120
+ const activity = await getWixelSessionActivity({
1121
+ ...base,
1122
+ sessionId: args.handle
1123
+ });
1124
+ pendingQuestion = activity.parkedQuestion;
1125
+ if (pendingQuestion)
1126
+ status = 'question';
1127
+ }
1128
+ console.log('[WixelResponseDump]', JSON.stringify({
1129
+ tool: 'get_wixel_turn_result',
1130
+ args,
1131
+ status,
1132
+ assets,
1133
+ editorUrl,
1134
+ pendingQuestionId: pendingQuestion?.toolCallId ?? null
1135
+ }).slice(0, 30000));
1136
+ if (args.live && (status === 'done' || status === 'failed')) {
1137
+ liveCards.forHandle(args.handle).finishedTurns.add(args.turnId);
1138
+ }
1139
+ const structuredContent = {
1140
+ success: true,
1141
+ status,
1142
+ turnId: args.turnId,
1143
+ assets,
1144
+ editorUrl,
1145
+ pendingQuestion,
1146
+ replyText,
1147
+ message: status === 'failed' ? replyText : null
1148
+ };
1149
+ return {
1150
+ isError: false,
1151
+ // Widget-only tool: the text IS the payload, so a host that drops
1152
+ // structuredContent (the widget bridge falls back to parsing the
1153
+ // first JSON text item) still delivers the status to the card.
1154
+ content: [
1155
+ { type: 'text', text: JSON.stringify(structuredContent) }
1156
+ ],
1157
+ structuredContent
1158
+ };
1159
+ }
1160
+ catch (error) {
1161
+ rethrowIfUnauthorized(error);
1162
+ console.error('❌ Error getting Wixel turn result:', error);
1163
+ const errorMessage = error.message;
1164
+ return {
1165
+ isError: true,
1166
+ content: [{ type: 'text', text: errorMessage }],
1167
+ structuredContent: { success: false, error: errorMessage }
1168
+ };
1169
+ }
1170
+ });
1171
+ }
1172
+ /**
1173
+ * Turn the asset ids and media URLs a reply delivered into renderable result
1174
+ * entries (video first, then decks, stills, documents) plus the editor deep
1175
+ * link. Shared by get_wixel_asset_result and get_wixel_turn_result.
1176
+ */
1177
+ async function resolveWixelAssetResults(args) {
1178
+ const { authHeaders } = args;
1179
+ const options = { conductorOverride: args.conductorOverride };
1180
+ // The model-facing schema caps these, but the live path feeds the resolver
1181
+ // straight from a reply's text — cap here too, or one oversized reply
1182
+ // fans out into an unbounded number of authenticated lookups.
1183
+ const cappedAssetIds = (args.assetIds ?? []).slice(0, MAX_RESULT_ITEMS);
1184
+ const cappedMediaUrls = (args.mediaUrls ?? []).slice(0, MAX_RESULT_ITEMS);
1185
+ // Best-effort: the project id only builds the editor deep link, so a
1186
+ // failing sessions endpoint must not sink a result whose media URLs
1187
+ // are perfectly renderable.
1188
+ let projectId = null;
1189
+ try {
1190
+ projectId = await getWixelSessionProjectId({
1191
+ authHeaders,
1192
+ conductorOverride: options.conductorOverride,
1193
+ sessionId: args.handle
1194
+ });
1195
+ }
1196
+ catch (projectError) {
1197
+ console.warn('[Wixel] editor link resolution failed:', projectError.message);
1198
+ }
1199
+ const editorUrl = projectId
1200
+ ? buildWixelEditorUrl(args.metaSiteId, projectId)
1201
+ : null;
1202
+ // Repeated ids/URLs would each cost their own request.
1203
+ const requestedAssetIds = Array.from(new Set(cappedAssetIds));
1204
+ const requestedMediaUrls = Array.from(new Set(cappedMediaUrls));
1205
+ // Short links resolve to the final media file (mp4 on wixmp etc.) so
1206
+ // the widget can play/show them inline. Anything that isn't allowlisted
1207
+ // Wix media is dropped rather than passed through.
1208
+ const resolvedMedia = (await mapWithConcurrency(requestedMediaUrls, RESULT_CONCURRENCY, async (raw) => {
1209
+ const parsed = parseWixelMediaUrl(raw);
1210
+ if (!parsed) {
1211
+ console.warn('[Wixel] rejected media URL', raw);
1212
+ return null;
1213
+ }
1214
+ const finalUrl = isWixelShortLink(parsed)
1215
+ ? await resolveWixelShortLink(parsed)
1216
+ : parsed.toString();
1217
+ if (!finalUrl)
1218
+ return null;
1219
+ return {
1220
+ url: finalUrl,
1221
+ kind: isVideoUrl(finalUrl)
1222
+ ? 'video'
1223
+ : isDocumentUrl(finalUrl)
1224
+ ? 'document'
1225
+ : 'image'
1226
+ };
1227
+ })).filter((media) => media !== null);
1228
+ // getAsset is project-scoped, so without a real project id every lookup
1229
+ // would be invalid — skip them and let the reply's media URLs carry the
1230
+ // result rather than returning nothing.
1231
+ if (requestedAssetIds.length && !projectId) {
1232
+ console.warn('[Wixel] skipping asset id lookups — project id unavailable');
1233
+ }
1234
+ // Fetch the entities first, keeping assetIds order — assigning media
1235
+ // URLs inside the concurrent lookups would pair them by completion
1236
+ // order instead.
1237
+ const entities = projectId
1238
+ ? await mapWithConcurrency(requestedAssetIds, RESULT_CONCURRENCY, async (assetId) => {
1239
+ try {
1240
+ return {
1241
+ assetId,
1242
+ asset: await getAsset({
1243
+ siteAuthHeaders: authHeaders,
1244
+ assetId,
1245
+ projectId,
1246
+ conductorOverride: options.conductorOverride
1247
+ })
1248
+ };
1249
+ }
1250
+ catch (assetError) {
1251
+ // An auth failure is the caller's problem to fix, not a
1252
+ // skippable asset — let it reach rethrowIfUnauthorized below.
1253
+ rethrowIfUnauthorized(assetError);
1254
+ console.warn('[Wixel] asset resolution failed', assetId, assetError.message);
1255
+ return { assetId, asset: null };
1256
+ }
1257
+ })
1258
+ : [];
1259
+ // Video assets carry no thumbnailUrl — their playable file arrives as a
1260
+ // media URL in the reply. Only pair them when the counts line up 1:1;
1261
+ // with a failed lookup in the mix there is no way to tell which URL
1262
+ // belongs to which asset, and guessing would label the wrong file.
1263
+ const videoMedia = resolvedMedia.filter((m) => m.kind === 'video');
1264
+ const videoEntities = entities.filter(({ asset }) => asset && String(asset.type) === 'VIDEO');
1265
+ const pairVideos = videoMedia.length === videoEntities.length;
1266
+ if (!pairVideos && videoMedia.length && videoEntities.length) {
1267
+ console.warn(`[Wixel] not pairing video media: ${videoEntities.length} video assets vs ${videoMedia.length} URLs`);
1268
+ }
1269
+ const videoMediaQueue = pairVideos ? [...videoMedia] : [];
1270
+ const usedUrls = new Set();
1271
+ // Pages are sibling assets, so they need a project-wide lookup.
1272
+ const deckIds = selectDeckIds(entities.flatMap(({ asset }) => (asset ? [asset] : [])));
1273
+ let decks = new Map();
1274
+ if (deckIds.size && projectId) {
1275
+ try {
1276
+ const { assets: projectAssets } = await queryAssets({
1277
+ siteAuthHeaders: authHeaders,
1278
+ projectId,
1279
+ conductorOverride: options.conductorOverride
1280
+ });
1281
+ decks = collectDecks((projectAssets ?? []), deckIds);
1282
+ }
1283
+ catch (deckError) {
1284
+ rethrowIfUnauthorized(deckError);
1285
+ console.warn('[Wixel] slide page lookup failed:', deckError.message);
1286
+ }
1287
+ }
1288
+ const seenDecks = new Set();
1289
+ let assets = entities.flatMap(({ assetId, asset }) => {
1290
+ if (!asset)
1291
+ return [];
1292
+ // One card per deck: several page ids are one deliverable.
1293
+ if (isSlideAsset(asset)) {
1294
+ const deckId = asset.parentId ?? asset.id ?? assetId;
1295
+ if (!deckIds.has(deckId))
1296
+ return [];
1297
+ const deck = decks.get(deckId);
1298
+ if (deck) {
1299
+ if (seenDecks.has(deckId))
1300
+ return [];
1301
+ seenDecks.add(deckId);
1302
+ deck.pageUrls.forEach((url) => usedUrls.add(url));
1303
+ return [
1304
+ {
1305
+ id: deckId,
1306
+ kind: 'slides',
1307
+ name: deck.name ?? asset.name ?? null,
1308
+ url: deck.pageUrls[0],
1309
+ slideUrls: deck.pageUrls
1310
+ }
1311
+ ];
1312
+ }
1313
+ // No pages resolved — fall through to the cover image.
1314
+ }
1315
+ const kind = String(asset.type) === 'VIDEO'
1316
+ ? 'video'
1317
+ : 'image';
1318
+ // A video card needs the playable file. Some video assets do carry a
1319
+ // thumbnailUrl (a poster frame), but rendering that as the video
1320
+ // would produce a card that never plays — so drop the entity instead
1321
+ // and let the reply's own video URLs stand in below.
1322
+ const url = (kind === 'video'
1323
+ ? (videoMediaQueue.shift()?.url ?? null)
1324
+ : (asset.thumbnailUrl ?? null)) ?? null;
1325
+ if (!url)
1326
+ return [];
1327
+ usedUrls.add(url);
1328
+ return [{ id: asset.id ?? assetId, kind, name: asset.name ?? null, url }];
1329
+ });
1330
+ // Media URLs stand in for assets we couldn't resolve, so append the
1331
+ // leftovers — but only for a kind no entity already produced, or an
1332
+ // animation reply (image id + video URL, plus the source image URL)
1333
+ // would render its still twice.
1334
+ const resolvedKinds = new Set(assets.map((asset) => asset.kind));
1335
+ const resolvedDeck = assets.some((asset) => asset.kind === 'slides');
1336
+ resolvedMedia.forEach((media, index) => {
1337
+ if (usedUrls.has(media.url) || resolvedKinds.has(media.kind))
1338
+ return;
1339
+ // A deck's reply cites imagery used inside its pages, and its own
1340
+ // PDF export — ingredients and duplicates, not extra deliverables.
1341
+ if (resolvedDeck && media.kind !== 'video')
1342
+ return;
1343
+ assets.push({
1344
+ id: `media-${index}`,
1345
+ kind: media.kind,
1346
+ name: null,
1347
+ url: media.url
1348
+ });
1349
+ });
1350
+ // Animating a still delivers BOTH the source image and the video, in
1351
+ // that order — but the video is the deliverable the user asked for, so
1352
+ // it leads, then decks, then loose stills. Image-only results keep
1353
+ // their reply order.
1354
+ assets = [
1355
+ ...assets.filter((asset) => asset.kind === 'video'),
1356
+ ...assets.filter((asset) => asset.kind === 'slides'),
1357
+ ...assets.filter((asset) => asset.kind === 'image'),
1358
+ ...assets.filter((asset) => asset.kind === 'document')
1359
+ ];
1360
+ return { assets, editorUrl };
1361
+ }
824
1362
  export function addGetWixelAssetResultTool(server, authStrategy, options = {}) {
1363
+ const liveCards = options.liveCards ?? createLiveCardRegistry();
825
1364
  const { getSiteAuthHeaders } = authStrategy;
826
1365
  registerAppTool(server, 'get_wixel_asset_result', {
827
1366
  title: 'Get Wixel asset result',
828
- description: `Resolve the assets a Wixel reply delivered into previewable results. Call this once a reply event delivers assets, passing the assetIds and mediaUrls EXACTLY as returned by list_wixel_events (never invent ids — a turnId is NOT an asset id).
829
- In widget-capable clients this renders the result card (preview image/video with edit and download actions); otherwise present the returned asset links as text, always including the Edit in Wixel link.`,
1367
+ description: `Render the result of a Wixel generation.
1368
+ In widget-capable clients call it RIGHT AFTER send_wixel_message with the returned turnId: it renders a live result card that shows generation progress, asks the user any questions Wixel has, and displays the final asset — no polling needed.
1369
+ Otherwise (or when a list_wixel_events reply already delivered assets) pass the assetIds and mediaUrls EXACTLY as returned by list_wixel_events (never invent ids — a turnId is NOT an asset id) and present the returned asset links as text, always including the Edit in Wixel link.`,
830
1370
  inputSchema: {
831
1371
  handle: z.string().describe('Session handle from start_wixel_session'),
832
1372
  metaSiteId: z
@@ -848,7 +1388,11 @@ In widget-capable clients this renders the result card (preview image/video with
848
1388
  .string()
849
1389
  .max(4000)
850
1390
  .optional()
851
- .describe("The user's original generation request, shown above the preview")
1391
+ .describe("The user's original generation request, shown above the preview"),
1392
+ turnId: z
1393
+ .string()
1394
+ .optional()
1395
+ .describe('The turn returned by send_wixel_message. Widget clients: pass this (without assetIds/mediaUrls) to render the live card while Wixel generates.')
852
1396
  },
853
1397
  annotations: {
854
1398
  readOnlyHint: true,
@@ -863,183 +1407,137 @@ In widget-capable clients this renders the result card (preview image/video with
863
1407
  });
864
1408
  const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
865
1409
  try {
866
- // Best-effort: the project id only builds the editor deep link, so a
867
- // failing sessions endpoint must not sink a result whose media URLs
868
- // are perfectly renderable.
869
- let projectId = null;
870
- try {
871
- projectId = await getWixelSessionProjectId({
872
- authHeaders,
873
- conductorOverride: options.conductorOverride,
874
- sessionId: args.handle
875
- });
876
- }
877
- catch (projectError) {
878
- console.warn('[Wixel] editor link resolution failed:', projectError.message);
879
- }
880
- const editorUrl = projectId
881
- ? buildWixelEditorUrl(args.metaSiteId, projectId)
882
- : null;
883
- // Repeated ids/URLs would each cost their own request.
884
- const requestedAssetIds = Array.from(new Set(args.assetIds ?? []));
885
- const requestedMediaUrls = Array.from(new Set(args.mediaUrls ?? []));
886
- // Short links resolve to the final media file (mp4 on wixmp etc.) so
887
- // the widget can play/show them inline. Anything that isn't allowlisted
888
- // Wix media is dropped rather than passed through.
889
- const resolvedMedia = (await mapWithConcurrency(requestedMediaUrls, RESULT_CONCURRENCY, async (raw) => {
890
- const parsed = parseWixelMediaUrl(raw);
891
- if (!parsed) {
892
- console.warn('[Wixel] rejected media URL', raw);
893
- return null;
894
- }
895
- const finalUrl = isWixelShortLink(parsed)
896
- ? await resolveWixelShortLink(parsed)
897
- : parsed.toString();
898
- if (!finalUrl)
899
- return null;
900
- return {
901
- url: finalUrl,
902
- kind: isVideoUrl(finalUrl)
903
- ? 'video'
904
- : isDocumentUrl(finalUrl)
905
- ? 'document'
906
- : 'image'
907
- };
908
- })).filter((media) => media !== null);
909
- // getAsset is project-scoped, so without a real project id every lookup
910
- // would be invalid — skip them and let the reply's media URLs carry the
911
- // result rather than returning nothing.
912
- if (requestedAssetIds.length && !projectId) {
913
- console.warn('[Wixel] skipping asset id lookups — project id unavailable');
914
- }
915
- // Fetch the entities first, keeping assetIds order — assigning media
916
- // URLs inside the concurrent lookups would pair them by completion
917
- // order instead.
918
- const entities = projectId
919
- ? await mapWithConcurrency(requestedAssetIds, RESULT_CONCURRENCY, async (assetId) => {
920
- try {
921
- return {
922
- assetId,
923
- asset: await getAsset({
924
- siteAuthHeaders: authHeaders,
925
- assetId,
926
- projectId,
927
- conductorOverride: options.conductorOverride
928
- })
929
- };
930
- }
931
- catch (assetError) {
932
- // An auth failure is the caller's problem to fix, not a
933
- // skippable asset — let it reach rethrowIfUnauthorized below.
934
- rethrowIfUnauthorized(assetError);
935
- console.warn('[Wixel] asset resolution failed', assetId, assetError.message);
936
- return { assetId, asset: null };
937
- }
938
- })
939
- : [];
940
- // Video assets carry no thumbnailUrl — their playable file arrives as a
941
- // media URL in the reply. Only pair them when the counts line up 1:1;
942
- // with a failed lookup in the mix there is no way to tell which URL
943
- // belongs to which asset, and guessing would label the wrong file.
944
- const videoMedia = resolvedMedia.filter((m) => m.kind === 'video');
945
- const videoEntities = entities.filter(({ asset }) => asset && String(asset.type) === 'VIDEO');
946
- const pairVideos = videoMedia.length === videoEntities.length;
947
- if (!pairVideos && videoMedia.length && videoEntities.length) {
948
- console.warn(`[Wixel] not pairing video media: ${videoEntities.length} video assets vs ${videoMedia.length} URLs`);
949
- }
950
- const videoMediaQueue = pairVideos ? [...videoMedia] : [];
951
- const usedUrls = new Set();
952
- // Pages are sibling assets, so they need a project-wide lookup.
953
- const deckIds = selectDeckIds(entities.flatMap(({ asset }) => asset ? [asset] : []));
954
- let decks = new Map();
955
- if (deckIds.size && projectId) {
956
- try {
957
- const { assets: projectAssets } = await queryAssets({
958
- siteAuthHeaders: authHeaders,
959
- projectId,
960
- conductorOverride: options.conductorOverride
961
- });
962
- decks = collectDecks((projectAssets ?? []), deckIds);
1410
+ if (!args.assetIds?.length && !args.mediaUrls?.length) {
1411
+ // Live mode: nothing to resolve yet — the widget follows the turn.
1412
+ if (!args.turnId) {
1413
+ return {
1414
+ isError: true,
1415
+ content: [
1416
+ {
1417
+ type: 'text',
1418
+ text: 'Pass the turnId from send_wixel_message (widget clients) or the assetIds/mediaUrls a reply delivered.'
1419
+ }
1420
+ ],
1421
+ structuredContent: { success: false, error: 'missing turnId' }
1422
+ };
963
1423
  }
964
- catch (deckError) {
965
- rethrowIfUnauthorized(deckError);
966
- console.warn('[Wixel] slide page lookup failed:', deckError.message);
1424
+ if (options.supportsWidgets?.() !== true) {
1425
+ return {
1426
+ isError: false,
1427
+ content: [
1428
+ {
1429
+ type: 'text',
1430
+ text: `This client cannot render the live card.
1431
+ <agent_instruction>Poll list_wixel_events (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25) until turnId "${args.turnId}" has a "reply" or "failed" event, then follow that response's instruction.</agent_instruction>`
1432
+ }
1433
+ ],
1434
+ structuredContent: {
1435
+ success: false,
1436
+ pending: true,
1437
+ turnId: args.turnId
1438
+ }
1439
+ };
967
1440
  }
968
- }
969
- const seenDecks = new Set();
970
- let assets = entities.flatMap(({ assetId, asset }) => {
971
- if (!asset)
972
- return [];
973
- // One card per deck: several page ids are one deliverable.
974
- if (isSlideAsset(asset)) {
975
- const deckId = asset.parentId ?? asset.id ?? assetId;
976
- if (!deckIds.has(deckId))
977
- return [];
978
- const deck = decks.get(deckId);
979
- if (deck) {
980
- if (seenDecks.has(deckId))
981
- return [];
982
- seenDecks.add(deckId);
983
- deck.pageUrls.forEach((url) => usedUrls.add(url));
984
- return [
1441
+ const record = liveCards.forHandle(args.handle);
1442
+ if (record.turns.has(args.turnId)) {
1443
+ // Already rendered for this turn — a second card would show the
1444
+ // same generation twice.
1445
+ console.log('[WixelResponseDump]', JSON.stringify({
1446
+ tool: 'get_wixel_asset_result',
1447
+ args,
1448
+ live: true,
1449
+ duplicate: true
1450
+ }));
1451
+ const duplicateContent = {
1452
+ success: true,
1453
+ pending: true,
1454
+ duplicate: true,
1455
+ turnId: args.turnId,
1456
+ assets: [],
1457
+ editorUrl: null,
1458
+ prompt: args.prompt ?? null
1459
+ };
1460
+ return {
1461
+ isError: false,
1462
+ content: [
1463
+ {
1464
+ type: 'text',
1465
+ text: `The live result card for this turn is already displayed.
1466
+ <agent_instruction>Do not call any Wixel tool again for this generation. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
1467
+ },
985
1468
  {
986
- id: deckId,
987
- kind: 'slides',
988
- name: deck.name ?? asset.name ?? null,
989
- url: deck.pageUrls[0],
990
- slideUrls: deck.pageUrls
1469
+ type: 'text',
1470
+ text: JSON.stringify(duplicateContent)
991
1471
  }
992
- ];
993
- }
994
- // No pages resolved — fall through to the cover image.
1472
+ ],
1473
+ structuredContent: duplicateContent
1474
+ };
995
1475
  }
996
- const kind = String(asset.type) === 'VIDEO'
997
- ? 'video'
998
- : 'image';
999
- // A video card needs the playable file. Some video assets do carry a
1000
- // thumbnailUrl (a poster frame), but rendering that as the video
1001
- // would produce a card that never plays — so drop the entity instead
1002
- // and let the reply's own video URLs stand in below.
1003
- const url = (kind === 'video'
1004
- ? (videoMediaQueue.shift()?.url ?? null)
1005
- : (asset.thumbnailUrl ?? null)) ?? null;
1006
- if (!url)
1007
- return [];
1008
- usedUrls.add(url);
1009
- return [
1010
- { id: asset.id ?? assetId, kind, name: asset.name ?? null, url }
1011
- ];
1012
- });
1013
- // Media URLs stand in for assets we couldn't resolve, so append the
1014
- // leftovers — but only for a kind no entity already produced, or an
1015
- // animation reply (image id + video URL, plus the source image URL)
1016
- // would render its still twice.
1017
- const resolvedKinds = new Set(assets.map((asset) => asset.kind));
1018
- const resolvedDeck = assets.some((asset) => asset.kind === 'slides');
1019
- resolvedMedia.forEach((media, index) => {
1020
- if (usedUrls.has(media.url) || resolvedKinds.has(media.kind))
1021
- return;
1022
- // A deck's reply cites imagery used inside its pages, and its own
1023
- // PDF export — ingredients and duplicates, not extra deliverables.
1024
- if (resolvedDeck && media.kind !== 'video')
1025
- return;
1026
- assets.push({
1027
- id: `media-${index}`,
1028
- kind: media.kind,
1029
- name: null,
1030
- url: media.url
1031
- });
1476
+ record.turns.set(args.turnId, Date.now());
1477
+ console.log('[WixelResponseDump]', JSON.stringify({ tool: 'get_wixel_asset_result', args, live: true }));
1478
+ const liveContent = {
1479
+ success: true,
1480
+ pending: true,
1481
+ turnId: args.turnId,
1482
+ assets: [],
1483
+ editorUrl: null,
1484
+ prompt: args.prompt ?? null
1485
+ };
1486
+ return {
1487
+ isError: false,
1488
+ content: [
1489
+ {
1490
+ type: 'text',
1491
+ text: `The live result card is displayed to the user. It shows generation progress, lets the user answer any question Wixel asks, and displays the final asset with edit/download actions — all inside the card.
1492
+ <agent_instruction>Do NOT poll list_wixel_events and do NOT call get_wixel_pending_question — the card handles the rest of this generation itself. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
1493
+ },
1494
+ { type: 'text', text: JSON.stringify(liveContent) }
1495
+ ],
1496
+ structuredContent: liveContent,
1497
+ _meta: widgetResultMeta(server)
1498
+ };
1499
+ }
1500
+ // A live card already displays these assets (list_wixel_events or
1501
+ // get_wixel_turn_result recorded them for a live turn) — rendering
1502
+ // them again would put the result in the chat twice.
1503
+ const shown = liveCards.get(args.handle)?.shownAssets;
1504
+ const requested = [...(args.assetIds ?? []), ...(args.mediaUrls ?? [])];
1505
+ if (options.supportsWidgets?.() === true &&
1506
+ shown?.size &&
1507
+ requested.every((item) => shown.has(item))) {
1508
+ console.log('[WixelResponseDump]', JSON.stringify({
1509
+ tool: 'get_wixel_asset_result',
1510
+ args,
1511
+ duplicate: true
1512
+ }));
1513
+ const duplicateContent = {
1514
+ success: true,
1515
+ duplicate: true,
1516
+ assets: [],
1517
+ editorUrl: null,
1518
+ prompt: args.prompt ?? null
1519
+ };
1520
+ return {
1521
+ isError: false,
1522
+ content: [
1523
+ {
1524
+ type: 'text',
1525
+ text: `This result is already displayed by the live result card.
1526
+ <agent_instruction>Do NOT present the asset again (no links, no image). End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
1527
+ },
1528
+ { type: 'text', text: JSON.stringify(duplicateContent) }
1529
+ ],
1530
+ structuredContent: duplicateContent
1531
+ };
1532
+ }
1533
+ const { assets, editorUrl } = await resolveWixelAssetResults({
1534
+ authHeaders,
1535
+ conductorOverride: options.conductorOverride,
1536
+ handle: args.handle,
1537
+ metaSiteId: args.metaSiteId,
1538
+ assetIds: args.assetIds,
1539
+ mediaUrls: args.mediaUrls
1032
1540
  });
1033
- // Animating a still delivers BOTH the source image and the video, in
1034
- // that order — but the video is the deliverable the user asked for, so
1035
- // it leads, then decks, then loose stills. Image-only results keep
1036
- // their reply order.
1037
- assets = [
1038
- ...assets.filter((asset) => asset.kind === 'video'),
1039
- ...assets.filter((asset) => asset.kind === 'slides'),
1040
- ...assets.filter((asset) => asset.kind === 'image'),
1041
- ...assets.filter((asset) => asset.kind === 'document')
1042
- ];
1043
1541
  console.log('[WixelResponseDump]', JSON.stringify({
1044
1542
  tool: 'get_wixel_asset_result',
1045
1543
  args,
@@ -1103,11 +1601,16 @@ In widget-capable clients this renders the result card (preview image/video with
1103
1601
  });
1104
1602
  }
1105
1603
  export function addWixelSessionTools(server, authStrategy, options = {}) {
1106
- addStartWixelSessionTool(server, authStrategy, options);
1107
- addSendWixelMessageTool(server, authStrategy, options);
1108
- addListWixelEventsTool(server, authStrategy, options);
1109
- addGetWixelPendingQuestionTool(server, authStrategy, options);
1110
- addGetWixelAssetResultTool(server, authStrategy, options);
1111
- addGetWixelStylePresetsTool(server, authStrategy, options);
1604
+ const shared = {
1605
+ ...options,
1606
+ liveCards: options.liveCards ?? createLiveCardRegistry()
1607
+ };
1608
+ addStartWixelSessionTool(server, authStrategy, shared);
1609
+ addSendWixelMessageTool(server, authStrategy, shared);
1610
+ addListWixelEventsTool(server, authStrategy, shared);
1611
+ addGetWixelPendingQuestionTool(server, authStrategy, shared);
1612
+ addGetWixelAssetResultTool(server, authStrategy, shared);
1613
+ addGetWixelStylePresetsTool(server, authStrategy, shared);
1614
+ addGetWixelTurnResultTool(server, authStrategy, shared);
1112
1615
  }
1113
1616
  //# sourceMappingURL=wixel-session-tools.js.map