@kolbo/mcp 1.77.3 → 1.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -31,7 +31,12 @@ function registerColorPaletteTools(server, client, options = {}) {
|
|
|
31
31
|
const palettes = result.color_palettes || [];
|
|
32
32
|
const text = JSON.stringify({ color_palettes: palettes, pagination: result.pagination }, null, 2);
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that
|
|
35
|
+
// renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows
|
|
36
|
+
// that carry no thumbnail field at all — and its BY_TOOL map still force-mounts
|
|
37
|
+
// the media grid on them, so the card rendered one broken-file glyph per cell.
|
|
38
|
+
// media.js and listResult() have always done it this way; these five lagged.
|
|
39
|
+
{
|
|
35
40
|
return uiResult(UI.mediaGrid, text, {
|
|
36
41
|
widget: 'media-grid',
|
|
37
42
|
title: 'Color DNA Palettes',
|
package/src/tools/generate.js
CHANGED
|
@@ -541,7 +541,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
541
541
|
// retired textToVideoGeneration path and was stale.
|
|
542
542
|
server.tool(
|
|
543
543
|
'generate_video',
|
|
544
|
-
'Generate a video from a text prompt using Kolbo AI. For SEVERAL different videos, pass all their prompts in `prompts` in ONE call (one combined widget) — never a series of separate calls. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports reference images (for style/composition guidance) and Visual DNA for character consistency. ROUTE BEFORE CALLING: when reference images anchor IDENTITY (specific characters, a specific product, a location that must match) — especially 2+ of them — that is generate_elements, not this tool; reference_images here are loose style/composition hints. Decide the right tool FIRST: a mis-routed call still starts a PAID generation, and switching tools afterwards without cancel_generation leaves the user paying for both. Returns the final video URL when complete.',
|
|
544
|
+
'Generate a video from a text prompt using Kolbo AI. For SEVERAL different videos, pass all their prompts in `prompts` in ONE call (one combined widget) — never a series of separate calls. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports reference images (for style/composition guidance) and Visual DNA for character consistency. Seedance 2/2.5 PERFORM quoted dialogue natively (synced voice, lip movement, room tone) — do not route scene dialogue to generate_speech or generate_lipsync; write it in ENGLISH (other languages, Hebrew included, do not perform reliably). Resolution is a credit MULTIPLIER (vs 720p: 480p x0.44, 1080p x2.25, 4k x4.95), so draft at 480p and re-run only the approved cut at delivery resolution. ROUTE BEFORE CALLING: when reference images anchor IDENTITY (specific characters, a specific product, a location that must match) — especially 2+ of them — that is generate_elements, not this tool; reference_images here are loose style/composition hints. Decide the right tool FIRST: a mis-routed call still starts a PAID generation, and switching tools afterwards without cancel_generation leaves the user paying for both. Returns the final video URL when complete.',
|
|
545
545
|
{
|
|
546
546
|
prompt: z.string().optional().describe('Text description of the video to generate. Required unless `prompts` is provided.'),
|
|
547
547
|
prompts: promptsField('videos'),
|
|
@@ -621,7 +621,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
621
621
|
// ─── generate_video_from_image ─────────────────────────────
|
|
622
622
|
server.tool(
|
|
623
623
|
'generate_video_from_image',
|
|
624
|
-
'Animate an existing still image into a video using Kolbo AI. The image comes from `image_url`; `prompt` describes the motion (not the subject — the subject is already in the image). For generating a video from scratch, use generate_video. ANIMATING SEVERAL SHOTS OF THE SAME SEQUENCE? Pass them ALL in `items` in ONE call (one combined widget) — never a series of separate calls, which buries the chat under one widget per clip. Across calls (a sequence longer than the batch cap), make the first call without `session_id`, take the `session_id` from its result, and pass that same `session_id` on every following call — otherwise each clip becomes its own session and the user gets a stack of near-identical single-clip sessions in the Kolbo sidebar. Returns the final video URL(s) when complete.',
|
|
624
|
+
'Animate an existing still image into a video using Kolbo AI. The image comes from `image_url`; `prompt` describes the motion (not the subject — the subject is already in the image). For generating a video from scratch, use generate_video. FOR A MULTI-SHOT FILM with recurring characters or locations, prefer ONE generate_elements call carrying the whole DNA-anchored sequence over a series of separate image-to-video clips. ANIMATING SEVERAL SHOTS OF THE SAME SEQUENCE? Pass them ALL in `items` in ONE call (one combined widget) — never a series of separate calls, which buries the chat under one widget per clip. Across calls (a sequence longer than the batch cap), make the first call without `session_id`, take the `session_id` from its result, and pass that same `session_id` on every following call — otherwise each clip becomes its own session and the user gets a stack of near-identical single-clip sessions in the Kolbo sidebar. Returns the final video URL(s) when complete.',
|
|
625
625
|
{
|
|
626
626
|
image_url: z.string().optional().describe('URL of the source image to animate. Required unless `items` is provided.'),
|
|
627
627
|
prompt: z.string().optional().describe('Text description of the desired MOTION (e.g., "camera slowly pans right while the character walks forward"). Required unless `items` is provided.'),
|
|
@@ -772,7 +772,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
772
772
|
// ─── generate_speech ───────────────────────────────────────
|
|
773
773
|
server.tool(
|
|
774
774
|
'generate_speech',
|
|
775
|
-
'Convert text to speech using Kolbo AI. Default provider is ElevenLabs. To pick a specific voice by language/gender, call list_voices first and pass the returned voice_id (or a voice display name — both work). Every voice belongs to a provider (ElevenLabs, DeepDub, MiniMax, Google/Gemini, OpenAI, Zonos) and each provider exposes its own expressive/style controls below — the engine ignores any control that does not apply to the chosen voice\'s provider, so it is safe to pass only what you need. Returns the final audio URL when complete.',
|
|
775
|
+
'Convert text to speech using Kolbo AI. SCOPE: narration, voiceover, and standalone audio (ads, explainers, audiobooks, announcements). This is NOT the route for character dialogue in a film you are generating with Seedance 2/2.5 — those models PERFORM quoted dialogue natively (synced voices, lip movement, room tone), so sending scene dialogue here just adds a second, unsynced voice track. Default provider is ElevenLabs. To pick a specific voice by language/gender, call list_voices first and pass the returned voice_id (or a voice display name — both work). Every voice belongs to a provider (ElevenLabs, DeepDub, MiniMax, Google/Gemini, OpenAI, Zonos) and each provider exposes its own expressive/style controls below — the engine ignores any control that does not apply to the chosen voice\'s provider, so it is safe to pass only what you need. Returns the final audio URL when complete.',
|
|
776
776
|
{
|
|
777
777
|
text: z.string().describe('The text to convert to speech'),
|
|
778
778
|
voice: z.string().optional().describe('Voice ID or display name — MUST come from a `list_voices` result, never constructed. Google/Gemini ids in particular are not validated provider-side: an id that is not in the catalog is silently mapped to another voice (or a default one) and the audio comes back in a voice nobody asked for. Do not pattern-match a locale onto an id you saw for another language. Default: "Rachel"'),
|
|
@@ -1185,7 +1185,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1185
1185
|
// ─── generate_elements ─────────────────────────────────────
|
|
1186
1186
|
server.tool(
|
|
1187
1187
|
'generate_elements',
|
|
1188
|
-
'Generate a video from reference elements (images, videos, and/or audio) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". PROMPT CONTRACT (Seedance / Elements): Locked Intro only — Total line, then [GLOBAL LOOK] / [CAST] / [LOCATION] / SHOT N. Do NOT write SCENE CONTEXT / OPTICS / ACTION department packs. Every Visual DNA in visual_dna_ids MUST also appear in the prompt as @ExactDNAName (e.g. "@Zohar walks…") — never "Zohar\'s" or "the man on the left" as a substitute. IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read elements_max_images / elements_max_videos / elements_max_audio on the chosen model before generating. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
|
|
1188
|
+
'Generate a video from reference elements (images, videos, and/or audio) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". PRIMARY ROUTE FOR A DNA-ANCHORED MULTI-SHOT FILM: one call can carry the whole sequence — seedance-2-5 takes 4-30s, up to 30 shots and 20 Visual DNAs in a SINGLE generation (seedance-2: 4-15s, 9 DNAs) — instead of a stack of separate clips. DIALOGUE IS PERFORMED NATIVELY: quoted dialogue in the prompt comes back as synced voices with lip movement, room tone and the SFX named in the AUDIO block — never route scene dialogue to generate_speech or generate_lipsync. Write dialogue in ENGLISH; other languages (Hebrew included) do not perform reliably. COST: resolution is a MULTIPLIER, not a flat fee (vs 720p: 480p x0.44, 1080p x2.25, 4k x4.95) — run blocking/draft passes at 480p and re-run only the approved cut at delivery resolution. PROMPT CONTRACT (Seedance / Elements): Locked Intro only — Total line, then [GLOBAL LOOK] / [CAST] / [LOCATION] / SHOT N. Do NOT write SCENE CONTEXT / OPTICS / ACTION department packs. Every Visual DNA in visual_dna_ids MUST also appear in the prompt as @ExactDNAName (e.g. "@Zohar walks…") — never "Zohar\'s" or "the man on the left" as a substitute. IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read elements_max_images / elements_max_videos / elements_max_audio on the chosen model before generating. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
|
|
1189
1189
|
{
|
|
1190
1190
|
prompt: z.string().describe('Locked Intro prompt (Seedance/Elements): Total line, [GLOBAL LOOK], [CAST] with @ExactDNAName for every visual_dna_ids entry, [LOCATION], then SHOT N. Not SCENE CONTEXT/OPTICS/ACTION packs. Never substitute "the left man" or "Zohar\'s" for @Name.'),
|
|
1191
1191
|
model: z.string().optional().describe('Model identifier. If the user already named a family (Grok / Kling / Veo / Seedance / …), pass THAT family — never default to Seedance because Elements often uses it. Use list_models type="elements" for exact ids and elements_max_* caps. Do NOT omit (omitting = Smart Select).'),
|
|
@@ -1462,7 +1462,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1462
1462
|
// ─── generate_lipsync ──────────────────────────────────────
|
|
1463
1463
|
server.tool(
|
|
1464
1464
|
'generate_lipsync',
|
|
1465
|
-
'Lipsync an audio track to a source image or video. Both `source` (image or video) and `audio` can be provided as URLs or as absolute local file paths. Pass a text_prompt only if the model supports it (some lipsync models do character performance from a prompt). **Validate before submitting: for `lipsync-video` sources, the input video duration must fall within `min_video_duration`-`max_video_duration` from list_models; audio duration must fall within `min_audio_duration`-`max_audio_duration` (and if `audio_max_follows_video_duration: true`, audio is also capped at the video duration); audio format must be in `supported_audio_formats` when specified.** Returns a lipsynced video URL.',
|
|
1465
|
+
'Lipsync an audio track to a source image or video. SCOPE: it syncs an EXISTING audio waveform to a face in footage you already have (dubbing, translation, fixing a delivered take). It is NOT how you get characters to speak in a film you are generating — Seedance 2/2.5 perform quoted dialogue natively with synced lip movement, so a scene generated there needs no lipsync pass. Both `source` (image or video) and `audio` can be provided as URLs or as absolute local file paths. Pass a text_prompt only if the model supports it (some lipsync models do character performance from a prompt). **Validate before submitting: for `lipsync-video` sources, the input video duration must fall within `min_video_duration`-`max_video_duration` from list_models; audio duration must fall within `min_audio_duration`-`max_audio_duration` (and if `audio_max_follows_video_duration: true`, audio is also capped at the video duration); audio format must be in `supported_audio_formats` when specified.** Returns a lipsynced video URL.',
|
|
1466
1466
|
{
|
|
1467
1467
|
source: z.string().describe('URL or absolute local path to the source image or video (the face to animate). For lipsync-video: duration must fall within `min_video_duration`-`max_video_duration` from list_models.'),
|
|
1468
1468
|
audio: z.string().describe('URL or absolute local path to the audio track (the voice to sync to). Duration must fall within `min_audio_duration`-`max_audio_duration` from list_models; format must be in `supported_audio_formats` (when set).'),
|
|
@@ -1581,7 +1581,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1581
1581
|
// ─── generate_video_from_video ─────────────────────────────
|
|
1582
1582
|
server.tool(
|
|
1583
1583
|
'generate_video_from_video',
|
|
1584
|
-
'Restyle / transform an existing video (video-to-video). Use for style transfer, scene restyling, subject swap, motion transfer, character replacement, or burning in styled subtitles (VEED Subtitles). Source video can be a URL or absolute local path. `prompt` is OPTIONAL: most models need it, but prompt-less models (VEED Subtitles, Act Two, Wan Animate, Kling Motion Control) ignore it. For VEED Subtitles, pass a `preset` style and optional `source_language` / `translation_language` instead of a prompt. IMPORTANT: different models support different extra inputs — call list_models type="video_to_video" and read max_images / max_videos / max_elements on the chosen model before generating. Pass reference_images for models with max_images > 0 (e.g. Kling O1/O3, Aleph, WAN VACE), reference_videos for models with max_videos > 1 (e.g. WAN 2.6 reference-to-video accepts up to 3), and elements for models with max_elements > 0. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
|
|
1584
|
+
'Restyle / transform an existing video (video-to-video). Use for style transfer, scene restyling, subject swap, motion transfer, character replacement, or burning in styled subtitles (VEED Subtitles). Source video can be a URL or absolute local path. `prompt` is OPTIONAL: most models need it, but prompt-less models (VEED Subtitles, Act Two, Wan Animate, Kling Motion Control) ignore it. For VEED Subtitles, pass a `preset` style and optional `source_language` / `translation_language` instead of a prompt. IMPORTANT: different models support different extra inputs — call list_models type="video_to_video" and read max_images / max_videos / max_elements on the chosen model before generating. Pass reference_images for models with max_images > 0 (e.g. Kling O1/O3, Aleph, WAN VACE), reference_videos for models with max_videos > 1 (e.g. WAN 2.6 reference-to-video accepts up to 3), and elements for models with max_elements > 0. REPAIR / RETIME (not restyling, no prompt needed — these keep the footage and fix or retime it): model "topaz/deblur/video" removes lens, motion and compression blur; "topaz/colorize/video" colorizes black-and-white footage; "topaz/interpolate/video" is SLOW MOTION and frame-rate conversion (see slowdown_factor / target_fps); "topaz/sdr-to-hdr/video" masters SDR footage to HDR (see output_format). Reach for these when the user says blurry, shaky-detail, black-and-white, slow motion, smoother frame rate, or HDR — a restyle model would repaint the video instead of repairing it. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
|
|
1585
1585
|
{
|
|
1586
1586
|
source_video: z.string().describe('URL or absolute local path to the primary source video to restyle. **Source duration must fall within `min_video_duration`-`max_video_duration` from list_models for the chosen model** — videos outside that range are rejected (or silently truncated by some upstream providers). For models that use reference_videos as their primary input (e.g. WAN 2.6 reference-to-video), pass the first reference video here and also include it in reference_videos.'),
|
|
1587
1587
|
prompt: z.string().optional().describe('Text description of the desired restyle / transformation. Required by most video-to-video models; omit for prompt-less models (VEED Subtitles, Act Two, Wan Animate, Kling Motion Control).'),
|
|
@@ -1898,7 +1898,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1898
1898
|
|
|
1899
1899
|
// ── upscale ────────────────────────────────────────────
|
|
1900
1900
|
scale: z.number().optional()
|
|
1901
|
-
.describe('Upscale factor: 1, 2
|
|
1901
|
+
.describe('Upscale factor: 1, 2 or 4. Used with operation="upscale". Default: 2. There is NO 8 — every Topaz split endpoint declares upscale_factor max 4 in its OpenAPI, so an 8 is a guaranteed provider error. Output size is otherwise unbounded: 4x on a 4K source is a 16K result and is priced pro-rata, not refused.'),
|
|
1902
1902
|
|
|
1903
1903
|
enhancement_model: z.string().optional()
|
|
1904
1904
|
.describe('Topaz engine. With operation="upscale" on model "topaz/upscale/image" it selects the engine family: "Standard V2" / "High Fidelity V3" / "CGI" / "Text Refine" (faithful), "Wonder 3.5" (rebuilds natural detail), "Bloom 2" (reinvents detail — most expensive), "Transparent" (keeps the alpha channel). With operation="enhance" it selects the correction engine for the chosen model. Omit for the model default. Call list_models to see the engines a model offers.'),
|
package/src/tools/presets.js
CHANGED
|
@@ -33,7 +33,12 @@ function registerPresetTools(server, client, options = {}) {
|
|
|
33
33
|
note: 'Filter with `type` (image | image_edit | video | music | text_to_video) to see a focused set. Pass the chosen exact id as `preset_id` on the next generation call.',
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that
|
|
37
|
+
// renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows
|
|
38
|
+
// that carry no thumbnail field at all — and its BY_TOOL map still force-mounts
|
|
39
|
+
// the media grid on them, so the card rendered one broken-file glyph per cell.
|
|
40
|
+
// media.js and listResult() have always done it this way; these five lagged.
|
|
41
|
+
{
|
|
37
42
|
return uiResult(UI.mediaGrid, text, {
|
|
38
43
|
widget: 'media-grid',
|
|
39
44
|
title: 'Presets' + (type ? ' — ' + type : ''),
|
|
@@ -78,7 +78,12 @@ function registerStockLibraryTools(server, client, options = {}) {
|
|
|
78
78
|
: '';
|
|
79
79
|
const text = `${head}\n\n${shownAssets.map(assetLine).join('\n\n')}${moreHint}\n\nUse [source:sourceId] with get_stock_asset for full variants, or import_stock_asset to copy it into the media library.`;
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that
|
|
82
|
+
// renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows
|
|
83
|
+
// that carry no thumbnail field at all — and its BY_TOOL map still force-mounts
|
|
84
|
+
// the media grid on them, so the card rendered one broken-file glyph per cell.
|
|
85
|
+
// media.js and listResult() have always done it this way; these five lagged.
|
|
86
|
+
{
|
|
82
87
|
const items = assets.slice(0, 24).map((a) => {
|
|
83
88
|
const mt = widgetMediaType(a.mediaType);
|
|
84
89
|
return {
|
|
@@ -160,7 +165,12 @@ function registerStockLibraryTools(server, client, options = {}) {
|
|
|
160
165
|
total: result.count,
|
|
161
166
|
});
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
// Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that
|
|
169
|
+
// renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows
|
|
170
|
+
// that carry no thumbnail field at all — and its BY_TOOL map still force-mounts
|
|
171
|
+
// the media grid on them, so the card rendered one broken-file glyph per cell.
|
|
172
|
+
// media.js and listResult() have always done it this way; these five lagged.
|
|
173
|
+
{
|
|
164
174
|
const collections = result.collections || [];
|
|
165
175
|
const items = collections.slice(0, 24).map((c) => ({
|
|
166
176
|
id: c.id,
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -207,7 +207,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
207
207
|
// ─── generate_character_sheet ──────────────────────────────
|
|
208
208
|
server.tool(
|
|
209
209
|
'generate_character_sheet',
|
|
210
|
-
'Generate a reference sheet for a Visual DNA from 1+ reference image URLs — the same step the in-app Visual DNA wizard offers, for EVERY DNA type via `sheet_type`: character = multi-angle turnaround, product = angles + branding/material/construction close-ups, environment = location angles + one signature detail, style = a style board (the same look applied to six varied subjects). The sheet is the single strongest consistency booster for a DNA, and it always preserves the reference\'s original art style (2D stays 2D, photo stays photo). CHARGES CREDITS, so when the user is about to create a DNA, OFFER this first ("want me to generate a reference sheet for stronger consistency? it costs a few credits") and only run it on a yes. Returns `character_sheet_url` — pass it as `character_sheet_url` to `create_visual_dna` with the matching `dna_type`.',
|
|
210
|
+
'STANDARD FIRST STEP OF THE ASSET PASS for any film/ad/scene: inventory the characters, locations and props the script needs, generate a sheet for each, create its Visual DNA from that sheet, confirm the whole set with the user, and only THEN generate video. Sheets for cinematic environments and invented characters run well on `mirage-film-2` (3cr); use `nano-banana-2` (10cr) or `gpt-image-2` (12cr) when reference fidelity or legible text matters. Generate a reference sheet for a Visual DNA from 1+ reference image URLs — the same step the in-app Visual DNA wizard offers, for EVERY DNA type via `sheet_type`: character = multi-angle turnaround, product = angles + branding/material/construction close-ups, environment = location angles + one signature detail, style = a style board (the same look applied to six varied subjects). The sheet is the single strongest consistency booster for a DNA, and it always preserves the reference\'s original art style (2D stays 2D, photo stays photo). CHARGES CREDITS, so when the user is about to create a DNA, OFFER this first ("want me to generate a reference sheet for stronger consistency? it costs a few credits") and only run it on a yes. Returns `character_sheet_url` — pass it as `character_sheet_url` to `create_visual_dna` with the matching `dna_type`.',
|
|
211
211
|
{
|
|
212
212
|
image_urls: z.array(z.string()).min(1).describe('Reference image URLs of the subject (for characters: front/side/varied angles work best). Use generated-image URLs or upload_media output.'),
|
|
213
213
|
sheet_type: z.enum(['character', 'character_headless', 'character_bible', 'product', 'environment', 'style']).optional().describe('Sheet layout. character = front/back/face turnaround. character_headless = wardrobe/body refs with a headless front panel (use when clothing must change without fighting the face sheet). character_bible = denser production model-sheet (turnaround + faces + wardrobe + color swatches). product / environment / style = matching DNA types. Defaults to character.')
|
package/src/tools/voices.js
CHANGED
|
@@ -80,7 +80,12 @@ function registerVoiceTools(server, client, options = {}) {
|
|
|
80
80
|
const range = `${start + 1}–${start + shownVoices.length}`;
|
|
81
81
|
const text = `Available voices (showing ${range} of ${voices.length}, page ${pageNum}/${pageCount}):\n\n${lines.join('\n\n')}${narrowHint}\n\nUse the "voice_id" value in generate_speech calls.`;
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that
|
|
84
|
+
// renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows
|
|
85
|
+
// that carry no thumbnail field at all — and its BY_TOOL map still force-mounts
|
|
86
|
+
// the media grid on them, so the card rendered one broken-file glyph per cell.
|
|
87
|
+
// media.js and listResult() have always done it this way; these five lagged.
|
|
88
|
+
{
|
|
84
89
|
return uiResult(UI.mediaGrid, text, {
|
|
85
90
|
widget: 'media-grid',
|
|
86
91
|
title: 'Voices',
|