@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.
- package/build/bin-standalone.js +10 -1
- package/build/cjs/index.cjs +645 -176
- package/build/cjs/toolkit.cjs +27 -8
- package/build/dts/code-mode/index.d.ts +7 -0
- package/build/dts/code-mode/index.js +14 -10
- package/build/dts/docs/get-to-know.js +17 -1
- package/build/dts/index.d.ts +1 -1
- package/build/dts/site-widget-tools/suggested-domain-tool/index.js +16 -5
- package/build/dts/toolkit-entry.d.cts +8 -0
- package/build/dts/toolkit-entry.d.ts +8 -0
- package/build/dts/toolkit.d.ts +3 -0
- package/build/dts/toolkit.js +3 -1
- package/build/dts/wixel/wixel-session-tools.d.ts +43 -0
- package/build/dts/wixel/wixel-session-tools.js +701 -198
- package/build/esm/index.js +645 -176
- package/build/esm/toolkit.js +27 -8
- package/package.json +2 -2
|
@@ -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
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
// the
|
|
490
|
-
//
|
|
491
|
-
const text =
|
|
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
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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: `
|
|
829
|
-
In widget-capable clients
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
987
|
-
|
|
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
|
-
|
|
1472
|
+
],
|
|
1473
|
+
structuredContent: duplicateContent
|
|
1474
|
+
};
|
|
995
1475
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|