@kolbo/mcp 1.88.4 → 1.89.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/README.md CHANGED
@@ -8,6 +8,10 @@ Use `list_fonts` / `get_font` to discover My Fonts. Local stdio clients upload o
8
8
 
9
9
  Only models reporting `supports_custom_fonts: true` accept these selections. Fonts and backend-generated specimens never go through media upload. See [Personal Fonts](https://docs.kolbo.ai/developer-api/personal-fonts). Availability requires a published client and deployed backend containing these tools; source changes alone do not update installed clients.
10
10
 
11
+ ## No-background images
12
+
13
+ `generate_image` and `generate_image_edit` accept `background: "transparent"` for models whose `list_models` record reports `supports_transparent_background: true`. Kolbo also appends the exact phrase `no background` once to the effective prompt, while the structured setting requests the native alpha channel through the configured provider route. Use `output_format: "png"` or `"webp"` (PNG is selected automatically when omitted). Prompt wording alone is not sufficient, and unsupported models reject the request instead of silently returning an opaque image.
14
+
11
15
  Use [Kolbo AI](https://kolbo.ai) as native tools in Claude Code and Claude Desktop via MCP (Model Context Protocol).
12
16
 
13
17
  Generate images, videos, music, speech, sound effects, multi-scene campaigns, and conversational chat — all from natural language in your coding environment. 100+ AI models behind Smart Select routing, with reusable Visual DNA profiles for character/style consistency.
@@ -278,7 +282,9 @@ Every generation tool also accepts an optional `project_id` arg that routes the
278
282
  | `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna or update_visual_dna |
279
283
  | `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
280
284
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
281
- | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Custom chat agents (reusable named personas; `description` is the system instruction) |
285
+ | `list_skills` / `create_skill` / `update_skill` / `delete_skill` | Skills — reusable named personas for the chat tool; `description` is the system instruction |
286
+ | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Same four tools under their original names. Still supported and unchanged; new integrations should use the `*_skill` names |
287
+ | `create_video_editor_session` / `export_video_editor_session` | Build an editable timeline from Kolbo-hosted clips, audio and text, then export it to MP4. No new media is generated; identical snapshots reuse the same export job |
282
288
  | `get_creative_director_status` | Re-check a Creative Director batch by generation_id until all parallel scenes finish (use after a `_timed_out` Director run) |
283
289
  | `list_sessions` | Enumerate sessions across all types, filterable by project, `type`, and `types[]` |
284
290
  | `rename_session` / `delete_session` / `restore_session` | Rename a session; soft-delete leftovers after a move; restore from trash |
@@ -307,4 +313,4 @@ Both are optional — the local install logs in via the browser on first use.
307
313
 
308
314
  ### Chat thinking level
309
315
 
310
- `chat_send_message` accepts optional `thinking_level`, using an ID from `list_models` with `type: "text"`. The server validates it against the resolved model; omitted or invalid values use `thinkingDefault`. Existing safeguards and legacy `deep_think` take precedence. Discover allowed levels through `thinkingLevels`; no package update is required when the server changes a model capability.
316
+ `chat_send_message` accepts optional `thinking_level`, using an ID from `list_models` with `type: "text"`. The server validates it against the resolved model; omitted or invalid values use `thinkingDefault`. Existing safeguards and legacy `deep_think` take precedence. Discover allowed levels through `thinkingLevels`; no package update is required when the server changes a model capability. For the Auto model, optional `routing_mode` accepts `fast`, `balanced`, or `smart`; omitted uses the server default of `balanced`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.88.4",
3
+ "version": "1.89.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -36,6 +36,22 @@ const BRIDGE_JS = `
36
36
  var queue = []; // deferred host-bound sends until initialized
37
37
  var hostContext = null;
38
38
  var readyFns = [], toolResultFns = [], themeFns = [], toolInputFns = [];
39
+ var latestResult, latestInput;
40
+ function deliver(f, args) {
41
+ try { f.apply(null, args); }
42
+ catch (e) {
43
+ console.error('Kolbo widget render failed', e);
44
+ var card = document.querySelector('.k-card');
45
+ var notice = document.getElementById('kolbo-render-error');
46
+ if (card && !notice) {
47
+ notice = document.createElement('div');
48
+ notice.id = 'kolbo-render-error';
49
+ notice.setAttribute('role', 'alert');
50
+ notice.textContent = 'This preview could not update. Your generation may still be running. Reopen this card or check your Kolbo library.';
51
+ card.appendChild(notice);
52
+ }
53
+ }
54
+ }
39
55
 
40
56
  function post(msg) { window.parent.postMessage(msg, '*'); }
41
57
 
@@ -67,11 +83,13 @@ const BRIDGE_JS = `
67
83
  return;
68
84
  }
69
85
  if (m.method === 'ui/notifications/tool-result') {
70
- toolResultFns.forEach(function (f) { try { f(m.params || {}); } catch (e) {} });
86
+ latestResult = m.params || {};
87
+ toolResultFns.forEach(function (f) { deliver(f, [latestResult]); });
71
88
  } else if (m.method === 'ui/notifications/tool-input' || m.method === 'ui/notifications/tool-input-partial') {
72
89
  // Fires while the tool is still RUNNING — lets widgets show a real
73
90
  // "preparing" state instead of a blank card until the result lands.
74
- toolInputFns.forEach(function (f) { try { f((m.params && m.params.arguments) || {}, m.params || {}); } catch (e) {} });
91
+ latestInput = [(m.params && m.params.arguments) || {}, m.params || {}];
92
+ toolInputFns.forEach(function (f) { deliver(f, latestInput); });
75
93
  } else if (m.method === 'ui/notifications/host-context-changed') {
76
94
  hostContext = (m.params && m.params.hostContext) || m.params || hostContext;
77
95
  themeFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
@@ -88,12 +106,13 @@ const BRIDGE_JS = `
88
106
  appInfo: { name: 'kolbo-widget', version: '1.0.0' },
89
107
  appCapabilities: {}
90
108
  }).then(function (res) {
109
+ if (oa) return; // A late JSON-RPC ack must not replace OpenAI host context.
91
110
  hostContext = (res && res.hostContext) || null;
92
111
  notify('ui/notifications/initialized');
93
112
  initialized = true;
94
113
  queue.forEach(post);
95
114
  queue = [];
96
- readyFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
115
+ readyFns.forEach(function (f) { deliver(f, [hostContext]); });
97
116
  }).catch(function () { /* host without apps support — widget stays static */ });
98
117
 
99
118
  var fsMode = false; // fullscreen: the HOST owns layout — size reports there
@@ -176,9 +195,9 @@ const BRIDGE_JS = `
176
195
  document.addEventListener('loadedmetadata', function () { queueSize(60); }, true);
177
196
 
178
197
  window.kolbo = {
179
- ready: function (f) { if (initialized) f(hostContext); else readyFns.push(f); },
180
- onToolResult: function (f) { toolResultFns.push(f); },
181
- onToolInput: function (f) { toolInputFns.push(f); },
198
+ ready: function (f) { if (initialized) deliver(f, [hostContext]); else readyFns.push(f); },
199
+ onToolResult: function (f) { toolResultFns.push(f); if (latestResult) deliver(f, [latestResult]); },
200
+ onToolInput: function (f) { toolInputFns.push(f); if (latestInput) deliver(f, latestInput); },
182
201
  onThemeChange: function (f) { themeFns.push(f); },
183
202
  callTool: function (name, args) { return request('tools/call', { name: name, arguments: args || {} }); },
184
203
  sendMessage: function (text) {
@@ -218,9 +237,13 @@ const BRIDGE_JS = `
218
237
  // came, and sat on "Preparing" with no prompt, no settings and no result —
219
238
  // even after the tool had returned. Everything here maps onto the same
220
239
  // callbacks the JSON-RPC path drives, so widgets need no host-specific code.
221
- var oa = window.openai && typeof window.openai === 'object' ? window.openai : null;
222
- if (oa) {
223
- var oaInputSeen = false, oaOutputSeen = false;
240
+ var oa = null;
241
+ var oaInputSeen, oaOutputSeen;
242
+ function attachOpenAI(event) {
243
+ var current = window.openai;
244
+ if (!current || typeof current !== 'object') return;
245
+ oa = current;
246
+ var globals = event && event.detail && event.detail.globals;
224
247
  var oaName = function () {
225
248
  var meta = oa.toolResponseMetadata || {};
226
249
  return oa.toolName || meta['kolbo/tool'] || (oa.toolOutput && oa.toolOutput.tool) || undefined;
@@ -232,18 +255,28 @@ const BRIDGE_JS = `
232
255
  if (!initialized) {
233
256
  initialized = true;
234
257
  queue = [];
235
- readyFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
258
+ readyFns.forEach(function (f) { deliver(f, [hostContext]); });
236
259
  } else {
237
260
  themeFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
238
261
  }
239
- if (oa.toolInput && !oaInputSeen) {
240
- oaInputSeen = true;
241
- toolInputFns.forEach(function (f) { try { f(oa.toolInput, { name: oaName(), arguments: oa.toolInput }); } catch (e) {} });
262
+ var input = globals && Object.prototype.hasOwnProperty.call(globals, 'toolInput') ? globals.toolInput : oa.toolInput;
263
+ var output = globals && Object.prototype.hasOwnProperty.call(globals, 'toolOutput') ? globals.toolOutput : oa.toolOutput;
264
+ var inputKey = JSON.stringify(input);
265
+ var metadata = globals && globals.toolResponseMetadata || oa.toolResponseMetadata || {};
266
+ var envelope = metadata.call_tool_result || metadata.mcp_tool_result;
267
+ var result = envelope && typeof envelope === 'object' ? Object.assign({}, envelope) : {};
268
+ if (output != null) result.structuredContent = output;
269
+ result._meta = Object.assign({}, result._meta || {}, metadata);
270
+ var outputKey = JSON.stringify(result);
271
+ if (input && inputKey !== oaInputSeen) {
272
+ oaInputSeen = inputKey;
273
+ latestInput = [input, { name: oaName(), arguments: input }];
274
+ toolInputFns.forEach(function (f) { deliver(f, latestInput); });
242
275
  }
243
- if (oa.toolOutput && !oaOutputSeen) {
244
- oaOutputSeen = true;
245
- var res = { structuredContent: oa.toolOutput, _meta: oa.toolResponseMetadata || {} };
246
- toolResultFns.forEach(function (f) { try { f(res); } catch (e) {} });
276
+ if ((output || envelope) && outputKey !== oaOutputSeen) {
277
+ oaOutputSeen = outputKey;
278
+ latestResult = result;
279
+ toolResultFns.forEach(function (f) { deliver(f, [latestResult]); });
247
280
  }
248
281
  };
249
282
  var oaCall = function (method) { return typeof oa[method] === 'function'; };
@@ -272,13 +305,14 @@ const BRIDGE_JS = `
272
305
  if (!oaCall('requestDisplayMode')) return Promise.resolve({ mode: 'inline' });
273
306
  return Promise.resolve(oa.requestDisplayMode({ mode: mode })).then(function (r) { return { mode: (r && r.mode) || mode }; });
274
307
  };
275
- window.addEventListener('openai:set_globals', oaSync);
276
308
  // Deferred: the widget's own script (which registers ready/onToolInput/
277
309
  // onToolResult) runs AFTER this bridge block in the same document, and a
278
310
  // synchronous sync here fired into empty listener lists and marked the
279
311
  // input/output as seen.
280
- setTimeout(oaSync, 0);
312
+ oaSync();
281
313
  }
314
+ window.addEventListener('openai:set_globals', attachOpenAI);
315
+ setTimeout(attachOpenAI, 0);
282
316
  })();
283
317
  `;
284
318
 
package/src/apps/html.js CHANGED
@@ -60,6 +60,8 @@ var ICONS = {
60
60
  file: _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>'),
61
61
  copy: _svg('<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'),
62
62
  chevronDown: _svg('<path d="M6 9l6 6 6-6"/>'),
63
+ chevronLeft: _svg('<path d="M15 18l-6-6 6-6"/>'),
64
+ chevronRight: _svg('<path d="M9 18l6-6-6-6"/>'),
63
65
  chevronUp: _svg('<path d="M18 15l-6-6-6 6"/>')
64
66
  };
65
67
  // Media-kind → icon (accepts model kind / media_type strings).
package/src/apps/index.js CHANGED
@@ -20,8 +20,8 @@ const { generationWidgetHtml } = require('./widgets/generation');
20
20
  const { mediaGridWidgetHtml } = require('./widgets/mediaGrid');
21
21
  const { catalogWidgetHtml } = require('./widgets/catalog');
22
22
  const { transcriptWidgetHtml } = require('./widgets/transcript');
23
- const { uploadWidgetHtml } = require('./widgets/upload');
24
- const { fontUploadWidgetHtml } = require('./widgets/fontUpload');
23
+ const { uploadWidgetHtml } = require('./widgets/upload');
24
+ const { fontUploadWidgetHtml } = require('./widgets/fontUpload');
25
25
  const { listWidgetHtml } = require('./widgets/list');
26
26
  const { HOST_MAP } = require('../cdn');
27
27
  const { plansWidgetHtml } = require('./widgets/plans');
@@ -31,8 +31,8 @@ const UI = {
31
31
  mediaGrid: 'ui://kolbo/media-grid.html',
32
32
  catalog: 'ui://kolbo/catalog.html',
33
33
  transcript: 'ui://kolbo/transcript.html',
34
- upload: 'ui://kolbo/upload.html',
35
- fontUpload: 'ui://kolbo/font-upload.html',
34
+ upload: 'ui://kolbo/upload.html',
35
+ fontUpload: 'ui://kolbo/font-upload.html',
36
36
  list: 'ui://kolbo/list.html',
37
37
  plans: 'ui://kolbo/plans.html',
38
38
  };
@@ -42,8 +42,8 @@ const WIDGET_BUILDERS = {
42
42
  [UI.mediaGrid]: mediaGridWidgetHtml,
43
43
  [UI.catalog]: catalogWidgetHtml,
44
44
  [UI.transcript]: transcriptWidgetHtml,
45
- [UI.upload]: uploadWidgetHtml,
46
- [UI.fontUpload]: fontUploadWidgetHtml,
45
+ [UI.upload]: uploadWidgetHtml,
46
+ [UI.fontUpload]: fontUploadWidgetHtml,
47
47
  [UI.list]: listWidgetHtml,
48
48
  [UI.plans]: plansWidgetHtml,
49
49
  };
@@ -734,8 +734,8 @@ const TOOL_WIDGETS = {
734
734
  // never saw it as widget-carrying. Result-level _meta alone isn't enough.
735
735
  list_color_palettes: UI.mediaGrid,
736
736
  // upload widget
737
- media_upload_widget: UI.upload,
738
- font_upload_widget: UI.fontUpload,
737
+ media_upload_widget: UI.upload,
738
+ font_upload_widget: UI.fontUpload,
739
739
  // generic list widget — flat record lists with no natural thumbnail
740
740
  list_projects: UI.list,
741
741
  // Must stay list.html — mapping this to generation.html mounts "Kolbo Generation /
package/src/apps/theme.js CHANGED
@@ -354,18 +354,64 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
354
354
  .k-input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
355
355
  .k-input::placeholder { color: var(--text-faint); }
356
356
 
357
- /* ---- Grid widget (media / stock / presets) ---- */
358
- .k-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }
359
- .k-cell { position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--border);
360
- background: var(--surface); cursor: pointer; transition: transform 250ms var(--spring); }
361
- .k-cell:hover { transform: translateY(-2px) scale(1.01); }
362
- .k-cell .k-cell-media { position: relative; aspect-ratio: 1; background: #000; }
363
- .k-cell .k-cell-media img { width: 100%; height: 100%; object-fit: contain; display: block; }
364
- .k-cell-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
365
- .k-cell .k-cell-label { padding: 6px 8px; font-size: 11px; color: var(--text-muted);
357
+ /* ---- Paged media grid (library / stock / presets / DNAs / moodboards) ----
358
+ Media-first tiles: the artwork IS the row. A title rides a gradient scrim
359
+ INSIDE the tile instead of a text block under it, so a page of results reads
360
+ as a contact sheet rather than a list of captions. No backdrop-filter on the
361
+ scrim for the same reason .k-gen-badge has none — a dozen blurred strips over
362
+ a dozen images forces a per-frame backdrop re-sample on phones. */
363
+ .k-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
364
+ /* Narrow hosts (Kolbo Code sidebar, phone): step down instead of shrinking
365
+ tiles below a thumbnail worth looking at. The 520px block further down wins
366
+ under 520 and takes it to 2. */
367
+ @media (max-width: 620px) { .k-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
368
+ .k-tile { position: relative; aspect-ratio: 1; border-radius: 12px; overflow: hidden;
369
+ border: 1px solid var(--border); background: var(--surface); cursor: pointer;
370
+ transition: transform 250ms var(--spring), box-shadow 250ms var(--smooth), border-color 150ms var(--smooth); }
371
+ .k-tile:hover { transform: translateY(-2px); border-color: var(--border-strong);
372
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.42); }
373
+ .k-tile img, .k-tile video { display: block; width: 100%; height: 100%; object-fit: cover; }
374
+ .k-tile-fb { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
375
+ color: var(--text-faint); font-size: 24px; }
376
+ .k-tile-cap { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; pointer-events: none;
377
+ padding: 20px 8px 7px; background: linear-gradient(transparent, rgba(0, 0, 0, 0.78)); }
378
+ .k-tile-t { font-size: 11px; font-weight: 600; color: #fff;
366
379
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
367
- .k-cell .k-cell-sub { padding: 0 8px 7px; font-size: 10px; color: var(--text-faint);
380
+ .k-tile-s { font-size: 10px; color: rgba(255, 255, 255, 0.66);
368
381
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
382
+ /* Hover actions, revealed on pointer devices and always-on where there is no
383
+ hover — a touch host must never hide the only Use affordance. */
384
+ .k-tile-acts { position: absolute; top: 6px; right: 6px; z-index: 3; display: flex; gap: 4px;
385
+ opacity: 0; transition: opacity 150ms var(--smooth); }
386
+ .k-tile:hover .k-tile-acts, .k-tile:focus-within .k-tile-acts { opacity: 1; }
387
+ @media (hover: none) { .k-tile-acts { opacity: 1; } }
388
+ .k-tile-act { width: 26px; height: 26px; padding: 0; border-radius: 7px;
389
+ border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(0, 0, 0, 0.62); color: #fff;
390
+ display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
391
+ transition: background 150ms var(--smooth); }
392
+ .k-tile-act:hover { background: var(--brand); border-color: var(--brand); }
393
+ .k-tile-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; }
394
+
395
+ /* ---- Pager (media grid + list) ----
396
+ A page of results at a fixed card height, instead of a card that grows a row
397
+ every time you ask for more. Forward past the last loaded page still goes
398
+ through the server's page_tool + next_args contract. */
399
+ .k-pager { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 12px; }
400
+ .k-pager-btn { width: 28px; height: 28px; padding: 0; border-radius: 8px;
401
+ border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
402
+ box-shadow: var(--specular); display: inline-flex; align-items: center; justify-content: center;
403
+ cursor: pointer; transition: background 150ms var(--smooth); }
404
+ .k-pager-btn:hover:not(:disabled) { background: var(--border-strong); }
405
+ .k-pager-btn:disabled { opacity: 0.3; cursor: default; }
406
+ .k-dots { display: flex; align-items: center; gap: 6px; }
407
+ .k-dot { width: 7px; height: 7px; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
408
+ background: var(--text); opacity: 0.28;
409
+ transition: width 220ms var(--spring), opacity 150ms var(--smooth); }
410
+ .k-dot:hover { opacity: 0.6; }
411
+ .k-dot.on { width: 22px; opacity: 0.95; cursor: default; }
412
+ .k-dot.ghost { cursor: default; opacity: 0.14; }
413
+ .k-pager-label { font-size: 11px; color: var(--text-faint);
414
+ font-family: 'JetBrains Mono', ui-monospace, monospace; min-width: 58px; text-align: center; }
369
415
 
370
416
  /* ---- Audio rows ---- */
371
417
  .k-audio-row { display: flex; align-items: center; gap: 10px; padding: 9px 10px;
@@ -405,6 +451,8 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
405
451
  .k-body { padding: 10px 12px 12px; }
406
452
  .k-footer { padding: 8px 12px 12px; }
407
453
  .k-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
454
+ .k-tile-act { width: 30px; height: 30px; }
455
+ .k-pager-btn { width: 34px; height: 34px; }
408
456
  .k-actions { flex-direction: column; align-items: stretch; }
409
457
  .k-actions .k-btn { width: 100%; min-height: 44px; justify-content: center; }
410
458
  .k-btn { min-height: 40px; padding: 10px 14px; }
@@ -659,6 +659,7 @@ function renderCancelled(creditsRefunded) {
659
659
  var MAX_POLL_MS = 35 * 60 * 1000;
660
660
  var MAX_POLL_ERRORS = 30;
661
661
  var pollStart = 0, pollErrors = 0;
662
+ var pollInFlight = false;
662
663
  var cancelRequested = false; // set by the Stop button; freezes the poll loop
663
664
 
664
665
  /* ---------- offscreen gate ----------
@@ -696,6 +697,7 @@ function releaseSeen() {
696
697
 
697
698
  function schedulePoll(sc) {
698
699
  if (cancelRequested) return;
700
+ if (!sc.generation_id && !(sc.generation_ids && sc.generation_ids.length) && !sc.status_args) return;
699
701
  if (!seen) { whenSeenFns.push(function () { schedulePoll(sc); }); return; }
700
702
  // The call itself long-waits server-side, for one transport-safe window
701
703
  // (~45s over the remote connector — see WAIT_WINDOW_MS in tools/generate.js).
@@ -707,12 +709,15 @@ function schedulePoll(sc) {
707
709
  pollTimer = setTimeout(function () { poll(sc); }, delay);
708
710
  }
709
711
  function poll(sc) {
710
- if (cancelRequested) return;
712
+ if (cancelRequested || pollInFlight) return;
711
713
  if (pollStart && (Date.now() - pollStart) > MAX_POLL_MS) {
712
714
  return renderTrackingIssue('This is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
713
715
  }
714
716
  var args = sc.status_args || { generation_id: sc.generation_id, wait: true };
717
+ pollInFlight = true;
715
718
  window.kolbo.callTool(sc.poll_tool || 'get_generation_status', args).then(function (res) {
719
+ pollInFlight = false;
720
+ if (cancelRequested || (state && (state.phase === 'completed' || state.phase === 'failed'))) return;
716
721
  var st = structured(res) || {};
717
722
  var stateName = st.state || st.phase || st.status;
718
723
  // A failed status CALL (tool error / not-found / {success:false}) is not a
@@ -766,6 +771,8 @@ function poll(sc) {
766
771
  schedulePoll(sc);
767
772
  }
768
773
  }).catch(function () {
774
+ pollInFlight = false;
775
+ if (cancelRequested || (state && (state.phase === 'completed' || state.phase === 'failed'))) return;
769
776
  if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue('Tracking paused after repeated connection errors. The generation may still be running.');
770
777
  schedulePoll(sc);
771
778
  });
@@ -1340,20 +1347,20 @@ var PRE_VIDEO_KEYS = ['source_video', 'reference_videos'];
1340
1347
  var PRE_AUDIO_KEYS = ['audio', 'audio_url', 'reference_audio_urls', 'seed_reference_audio_urls'];
1341
1348
 
1342
1349
  // The card mounts the moment the tool is CALLED, so the only thing it knows is
1343
- // the raw tool input - and for an edit/elements call the submit that follows is
1350
+ // the raw tool input — and for an edit/elements call the submit that follows is
1344
1351
  // the LONGEST wait on the card (local files are re-hosted first). Map the input
1345
1352
  // onto the same shape renderChips already reads for a server payload so the
1346
1353
  // references, DNA count and settings are on screen immediately instead of after
1347
1354
  // a minute of blank skeleton.
1348
1355
  // Deliberately NOT shown here: model, voice, DNA and moodboard NAMES. Those are
1349
1356
  // resolved server-side and arrive with the result (which overwrites all of
1350
- // this) - rendering the raw identifier the caller passed would put an id on the
1357
+ // this) — rendering the raw identifier the caller passed would put an id on the
1351
1358
  // card, which is never allowed.
1352
1359
  function preRefSc(toolName, a) {
1353
1360
  var img = [], vid = [], aud = [];
1354
1361
  var take = function (v, bucket) {
1355
1362
  if (typeof v === 'string') {
1356
- // http(s) only - an absolute local path is not loadable from the iframe.
1363
+ // http(s) only — an absolute local path is not loadable from the iframe.
1357
1364
  if (/^https?:/i.test(v) && bucket.indexOf(v) < 0) bucket.push(v);
1358
1365
  return;
1359
1366
  }
@@ -1484,6 +1491,8 @@ function completedFromPlain(sc) {
1484
1491
  }
1485
1492
 
1486
1493
  /* ---------- wire host events ---------- */
1494
+ // Paint before the host handshake: tool input may arrive late or be withheld.
1495
+ bootPre(null, null);
1487
1496
  window.kolbo.onToolResult(function (result) {
1488
1497
  var sc = result.structuredContent || structured(result);
1489
1498
  var list = listPayload(sc);
@@ -7,7 +7,8 @@ const { widgetPage } = require('../html');
7
7
  * with no natural thumbnail (projects, sessions, agents, docs, folders,
8
8
  * knowledge-base sources). Reuses the exact row shell mediaGrid's audio rows
9
9
  * and catalog's model rows already use (`.k-audio-row` / `.k-audio-meta` /
10
- * `.k-chip` / `.k-btn` — no new CSS needed).
10
+ * `.k-chip` / `.k-btn` — no new CSS needed), and the same pager the media grid
11
+ * uses, so a long list is a fixed-height page instead of a card that grows.
11
12
  *
12
13
  * structuredContent contract:
13
14
  * {
@@ -43,6 +44,11 @@ const SCRIPT = `
43
44
  el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
44
45
  el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
45
46
  var state = null;
47
+ var page = 0;
48
+ var PAGE = 8;
49
+
50
+ function pageCount() { return Math.max(1, Math.ceil(((state && state.items) || []).length / PAGE)); }
51
+ function hasMore() { return !!(state && state.page_tool && state.next_args); }
46
52
 
47
53
  function boot(sc) {
48
54
  var list = listPayload(sc);
@@ -50,49 +56,73 @@ function boot(sc) {
50
56
  // listPayload() normalises plain shapes; carry the paging contract through.
51
57
  if (sc && sc.page_tool && !list.page_tool) { list.page_tool = sc.page_tool; list.next_args = sc.next_args; list.page = sc.page; }
52
58
  state = list;
59
+ page = 0;
53
60
  el('title').textContent = list.title || 'List';
54
- var total = list.total != null ? list.total : list.items.length;
61
+ render();
62
+ return true;
63
+ }
64
+
65
+ function render() {
66
+ var total = state.total != null ? state.total : state.items.length;
55
67
  el('count-chip').style.display = '';
56
68
  el('count-chip').textContent = total + (total === 1 ? ' item' : ' items');
57
- if (!list.items.length) {
69
+ if (!state.items.length) {
58
70
  el('stage').classList.add('k-empty');
59
71
  el('stage').innerHTML = 'Nothing here yet';
60
72
  window.kolbo.notifySize();
61
- return true;
62
- }
63
- // Render every item the server sent (the old slice(0, 40) silently dropped
64
- // rows, and nothing told the user). Load more only when the server said how.
65
- var h = list.items.map(itemHTML).join('');
66
- var shown = list.items.length;
67
- if (list.page_tool && list.next_args) {
68
- h += '<button class="k-btn" id="load-more" style="width:100%;margin-top:10px">Load more' +
69
- (total > shown ? ' (' + shown + ' of ' + total + ' shown)' : '') + '</button>';
73
+ return;
70
74
  }
75
+ if (page > pageCount() - 1) page = pageCount() - 1;
76
+ var h = state.items.slice(page * PAGE, page * PAGE + PAGE).map(itemHTML).join('') + pagerHTML();
71
77
  el('stage').innerHTML = h;
72
78
  el('stage').classList.remove('k-empty');
73
79
  wire();
74
- var lm = el('load-more');
75
- if (lm) lm.onclick = function () { fetchNextPage(lm); };
76
80
  window.kolbo.notifySize();
77
- return true;
81
+ }
82
+
83
+ function pagerHTML() {
84
+ var n = pageCount();
85
+ var more = hasMore();
86
+ if (n <= 1 && !more) return '';
87
+ var mid;
88
+ if (n <= 8) {
89
+ var dots = '';
90
+ for (var i = 0; i < n; i++) {
91
+ dots += '<button class="k-dot' + (i === page ? ' on' : '') + '" data-page="' + i + '" aria-label="Page ' + (i + 1) + '"></button>';
92
+ }
93
+ if (more) dots += '<span class="k-dot ghost"></span>';
94
+ mid = '<div class="k-dots">' + dots + '</div>';
95
+ } else {
96
+ mid = '<span class="k-pager-label">' + (page + 1) + ' / ' + n + (more ? '+' : '') + '</span>';
97
+ }
98
+ return '<div class="k-pager">' +
99
+ '<button class="k-pager-btn" id="page-prev" aria-label="Previous"' + (page === 0 ? ' disabled' : '') + '>' + ICONS.chevronLeft + '</button>' +
100
+ mid +
101
+ '<button class="k-pager-btn" id="page-next" aria-label="Next"' + (page >= n - 1 && !more ? ' disabled' : '') + '>' + ICONS.chevronRight + '</button>' +
102
+ '</div>';
78
103
  }
79
104
 
80
105
  function fetchNextPage(btn) {
81
106
  if (!state || !state.page_tool || !state.next_args || btn.disabled) return;
82
107
  btn.disabled = true;
83
- var label = btn.textContent;
84
- btn.innerHTML = '<span class="k-spin"></span> Loading';
108
+ btn.innerHTML = '<span class="k-spin"></span>';
85
109
  window.kolbo.callTool(state.page_tool, state.next_args).then(function (res) {
86
110
  var sc = structured(res);
87
111
  var next = sc ? listPayload(sc) || sc : null;
88
112
  var more = (next && next.items) || [];
89
- if ((res && res.isError) || !next) { btn.disabled = false; btn.textContent = 'Could not load more — try again'; return; }
90
- if (!more.length) { btn.textContent = 'No more results'; return; }
113
+ if ((res && res.isError) || !next) {
114
+ btn.disabled = false;
115
+ btn.innerHTML = ICONS.chevronRight;
116
+ btn.title = 'Could not load more — try again';
117
+ return;
118
+ }
119
+ if (!more.length) { state.next_args = undefined; render(); return; }
91
120
  state.items = state.items.concat(more);
92
121
  state.next_args = sc.next_args;
93
122
  if (sc.total != null) state.total = sc.total;
94
- boot(Object.assign({}, state, { widget: 'list', page_tool: state.page_tool, next_args: state.next_args }));
95
- }).catch(function () { btn.disabled = false; btn.textContent = label; });
123
+ page = page + 1;
124
+ render();
125
+ }).catch(function () { btn.disabled = false; btn.innerHTML = ICONS.chevronRight; });
96
126
  }
97
127
 
98
128
  function apply(result) {
@@ -101,7 +131,8 @@ function apply(result) {
101
131
  return boot(inner.structuredContent || structured(inner) || inner);
102
132
  }
103
133
 
104
- function itemHTML(item, i) {
134
+ function itemHTML(item) {
135
+ var i = state.items.indexOf(item);
105
136
  var clickable = !!item.id;
106
137
  var avatar = item.thumbnail
107
138
  ? '<img class="k-audio-art k-peek-hit" src="' + esc(item.thumbnail) + '" alt="" loading="lazy"'
@@ -119,14 +150,15 @@ function itemHTML(item, i) {
119
150
  }
120
151
 
121
152
  function wire() {
122
- bindPeekHits(el('stage'));
123
- Array.prototype.forEach.call(document.querySelectorAll('[data-open]'), function (b) {
153
+ var stage = el('stage');
154
+ bindPeekHits(stage);
155
+ Array.prototype.forEach.call(stage.querySelectorAll('[data-open]'), function (b) {
124
156
  b.onclick = function (e) {
125
157
  e.stopPropagation();
126
158
  window.kolbo.openLink(state.items[+b.getAttribute('data-open')].open_url);
127
159
  };
128
160
  });
129
- Array.prototype.forEach.call(document.querySelectorAll('.k-audio-row'), function (row) {
161
+ Array.prototype.forEach.call(stage.querySelectorAll('.k-audio-row'), function (row) {
130
162
  var item = state.items[+row.getAttribute('data-i')];
131
163
  if (!item || !item.id) return;
132
164
  row.onclick = function (e) {
@@ -134,6 +166,16 @@ function wire() {
134
166
  window.kolbo.insertText(String(item.id));
135
167
  };
136
168
  });
169
+ Array.prototype.forEach.call(stage.querySelectorAll('[data-page]'), function (d) {
170
+ d.onclick = function () { var p = +d.getAttribute('data-page'); if (p !== page) { page = p; render(); } };
171
+ });
172
+ var prev = el('page-prev');
173
+ if (prev) prev.onclick = function () { if (page > 0) { page--; render(); } };
174
+ var next = el('page-next');
175
+ if (next) next.onclick = function () {
176
+ if (page < pageCount() - 1) { page++; render(); return; }
177
+ fetchNextPage(next);
178
+ };
137
179
  }
138
180
 
139
181
  window.kolbo.onToolResult(function (result) {