@luckydraw/cumulus 0.31.66 → 1.0.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +12 -556
  2. package/LICENSE +150 -0
  3. package/README.md +27 -8
  4. package/dist/gateway/adapters/webchat.d.ts +15 -0
  5. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  6. package/dist/gateway/adapters/webchat.js +78 -5
  7. package/dist/gateway/adapters/webchat.js.map +1 -1
  8. package/dist/gateway/config.d.ts +17 -2
  9. package/dist/gateway/config.d.ts.map +1 -1
  10. package/dist/gateway/config.js +10 -3
  11. package/dist/gateway/config.js.map +1 -1
  12. package/dist/gateway/daemon.d.ts +3 -1
  13. package/dist/gateway/daemon.d.ts.map +1 -1
  14. package/dist/gateway/daemon.js +128 -39
  15. package/dist/gateway/daemon.js.map +1 -1
  16. package/dist/gateway/namespaces.d.ts +34 -0
  17. package/dist/gateway/namespaces.d.ts.map +1 -1
  18. package/dist/gateway/namespaces.js +58 -0
  19. package/dist/gateway/namespaces.js.map +1 -1
  20. package/dist/gateway/server.d.ts +8 -0
  21. package/dist/gateway/server.d.ts.map +1 -1
  22. package/dist/gateway/server.js +150 -41
  23. package/dist/gateway/server.js.map +1 -1
  24. package/dist/gateway/setup.d.ts +32 -0
  25. package/dist/gateway/setup.d.ts.map +1 -1
  26. package/dist/gateway/setup.js +23 -3
  27. package/dist/gateway/setup.js.map +1 -1
  28. package/dist/gateway/static/blex-render.js +341 -0
  29. package/dist/gateway/static/chat.html +1 -0
  30. package/dist/gateway/static/widget.js +1009 -738
  31. package/dist/lib/gateway.d.ts +30 -8
  32. package/dist/lib/gateway.d.ts.map +1 -1
  33. package/dist/lib/gateway.js +36 -11
  34. package/dist/lib/gateway.js.map +1 -1
  35. package/dist/lib/history.d.ts +22 -0
  36. package/dist/lib/history.d.ts.map +1 -1
  37. package/dist/lib/history.js +59 -21
  38. package/dist/lib/history.js.map +1 -1
  39. package/dist/lib/huggingface-provider.d.ts.map +1 -1
  40. package/dist/lib/huggingface-provider.js +11 -3
  41. package/dist/lib/huggingface-provider.js.map +1 -1
  42. package/dist/lib/license.d.ts +76 -0
  43. package/dist/lib/license.d.ts.map +1 -0
  44. package/dist/lib/license.js +141 -0
  45. package/dist/lib/license.js.map +1 -0
  46. package/docs/agentic-harness-primer.md +283 -0
  47. package/docs/conditional-continuation.md +167 -0
  48. package/docs/web-app-agent-guide.md +559 -0
  49. package/examples/web-app-agent/README.md +334 -0
  50. package/examples/web-app-agent/agent/mcp-shim.js +105 -0
  51. package/examples/web-app-agent/gateway.config.example.json +70 -0
  52. package/examples/web-app-agent/package.json +13 -0
  53. package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
  54. package/examples/web-app-agent/public/agent/bridge-mount.js +91 -0
  55. package/examples/web-app-agent/public/agent/chat-client.js +104 -0
  56. package/examples/web-app-agent/public/agent/commands.js +256 -0
  57. package/examples/web-app-agent/public/agent/device-thread.js +48 -0
  58. package/examples/web-app-agent/public/agent/panel.css +113 -0
  59. package/examples/web-app-agent/public/agent/panel.js +392 -0
  60. package/examples/web-app-agent/public/app.js +250 -0
  61. package/examples/web-app-agent/public/index.html +126 -0
  62. package/examples/web-app-agent/server.js +379 -0
  63. package/package.json +7 -3
@@ -177,6 +177,7 @@
177
177
  [
178
178
  { provider: 'openai', key: 'openaiApiKey' },
179
179
  { provider: 'huggingface', key: 'hfApiKey' },
180
+ { provider: 'license', key: 'licenseKey' },
180
181
  ].forEach(function (entry) {
181
182
  var credential = credentials[entry.provider] || {};
182
183
  var value = String(credential.value || '').trim();
@@ -186,6 +187,48 @@
186
187
  return payload;
187
188
  }
188
189
 
190
+ // ─── Default-application rules (task 130, Option C) ─────────────────────────
191
+ // One gateway default across the whole catalog. Picking an Anthropic model as
192
+ // the gateway default forcibly couples the Claude sub-model default to it —
193
+ // when the gateway default IS Anthropic, `model: 'claude'` +
194
+ // `claudeModels[].default` are the same knob and cannot diverge.
195
+ function applyGatewayDefault(catalog, modelId) {
196
+ var chosen = null;
197
+ catalog.forEach(function (model) {
198
+ model.gatewayDefault = model.id === modelId;
199
+ if (model.gatewayDefault) chosen = model;
200
+ });
201
+ if (chosen && chosen.provider === 'anthropic') {
202
+ catalog.forEach(function (model) {
203
+ if (model.provider === 'anthropic') model.providerDefault = model.id === modelId;
204
+ });
205
+ }
206
+ return chosen;
207
+ }
208
+
209
+ function applyClaudeSubDefault(catalog, modelId) {
210
+ catalog.forEach(function (model) {
211
+ if (model.provider === 'anthropic') model.providerDefault = model.id === modelId;
212
+ });
213
+ }
214
+
215
+ // The Claude sub-model default is a visible control ONLY when it can actually
216
+ // diverge from the gateway default: the gateway default is non-Anthropic
217
+ // (else coupling forces equality) and there are ≥2 Anthropic models (else
218
+ // there is no choice to make).
219
+ function claudeSubDefaultVisible(catalog) {
220
+ var anthropicCount = 0;
221
+ for (var i = 0; i < catalog.length; i++) {
222
+ if (catalog[i].provider === 'anthropic') anthropicCount++;
223
+ }
224
+ if (anthropicCount < 2) return false;
225
+ var gatewayDefault = null;
226
+ for (var j = 0; j < catalog.length; j++) {
227
+ if (catalog[j].gatewayDefault) gatewayDefault = catalog[j];
228
+ }
229
+ return !gatewayDefault || gatewayDefault.provider !== 'anthropic';
230
+ }
231
+
189
232
  // ─── Styles ─────────────────────────────────────────────────────────────────
190
233
  var STYLES = [
191
234
  '.cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }',
@@ -366,6 +409,19 @@
366
409
  '.blex-block-container[data-blex-ready="true"] {',
367
410
  ' min-height: auto;',
368
411
  '}',
412
+ /* Raw-fence fallback: shown when nothing claims the container (user message,
413
+ changelog, or blex.min.js failed to load). Better a visible fence than the
414
+ blank rectangle min-height used to produce. */
415
+ '.blex-fallback {',
416
+ ' margin: 0; padding: 0.6em 0.8em;',
417
+ ' background: rgba(255,255,255,0.04);',
418
+ ' border: 1px solid rgba(255,255,255,0.08);',
419
+ ' border-radius: 8px;',
420
+ ' font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;',
421
+ ' font-size: 0.82em; line-height: 1.45;',
422
+ ' white-space: pre-wrap; word-break: break-word;',
423
+ ' opacity: 0.85;',
424
+ '}',
369
425
  /* Blex CSS isolation — revert widget resets inside blex blocks */
370
426
  '.blex-block-container * { margin: revert; padding: revert; line-height: revert; list-style: revert; border-spacing: revert; border: revert; }',
371
427
  '.blex-block-container { isolation: isolate; }',
@@ -1197,6 +1253,12 @@
1197
1253
  ' text-decoration: underline; text-underline-offset: 2px;',
1198
1254
  '}',
1199
1255
  '.cumulus-topbar-changelog:hover { color: #ccc; background: #3a3a3a; }',
1256
+ '.cumulus-topbar-settings {',
1257
+ ' background: none; border: none; color: #888; cursor: pointer;',
1258
+ ' font-size: 15px; padding: 2px 5px; line-height: 1;',
1259
+ ' border-radius: 3px; transition: color 0.2s, background 0.2s;',
1260
+ '}',
1261
+ '.cumulus-topbar-settings:hover { color: #ccc; background: #3a3a3a; }',
1200
1262
  '.cumulus-changelog-popover {',
1201
1263
  ' position: absolute; top: 44px; left: 12px; z-index: 1000;',
1202
1264
  ' width: min(480px, calc(100vw - 24px)); max-height: 70vh; overflow-y: auto;',
@@ -1315,137 +1377,188 @@
1315
1377
  '.cumulus-model-popover select { max-width: 152px; margin: 0; }',
1316
1378
 
1317
1379
  /* ── Settings modal (provider-organized model catalog + credentials) ── */
1380
+ /* These three roots mount on document.body, OUTSIDE .cumulus-widget, so they
1381
+ must carry the font stack themselves or the browser falls back to serif. */
1382
+ '.cumulus-settings-backdrop, .cumulus-context-menu, .cumulus-confirm-dialog {',
1383
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
1384
+ ' -webkit-font-smoothing: antialiased;',
1385
+ '}',
1318
1386
  '.cumulus-settings-backdrop {',
1319
- ' position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.55);',
1387
+ ' position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.6);',
1388
+ ' backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);',
1320
1389
  ' display: flex; align-items: center; justify-content: center; padding: 20px;',
1390
+ ' font-size: 13px; line-height: 1.5; color: #e3e5ea;',
1321
1391
  '}',
1322
1392
  '.cumulus-settings-modal {',
1323
- ' background: #232323; border: 1px solid #3d3d3d; border-radius: 10px;',
1393
+ ' background: #1f2023; border: 1px solid #313338; border-radius: 14px;',
1324
1394
  ' width: min(720px, 100%); max-height: 86vh; display: flex; flex-direction: column;',
1325
- ' box-shadow: 0 16px 48px rgba(0,0,0,0.6); overflow: hidden;',
1395
+ ' box-shadow: 0 24px 64px rgba(0,0,0,0.55); overflow: hidden;',
1326
1396
  '}',
1327
1397
  '.cumulus-settings-header {',
1328
1398
  ' display: flex; align-items: flex-start; justify-content: space-between;',
1329
- ' padding: 14px 18px; border-bottom: 1px solid #333; flex-shrink: 0;',
1399
+ ' padding: 16px 20px 14px; border-bottom: 1px solid #2a2b30; flex-shrink: 0;',
1330
1400
  '}',
1331
- '.cumulus-settings-title { font-weight: 600; font-size: 15px; color: #ededed; }',
1332
- '.cumulus-settings-subtitle { font-size: 12px; color: #888; margin-top: 3px; }',
1333
- '.cumulus-settings-body { padding: 6px 18px 10px; overflow-y: auto; flex: 1; }',
1401
+ '.cumulus-settings-title { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; color: #f2f3f5; }',
1402
+ '.cumulus-settings-subtitle { font-size: 12px; color: #8b8f96; margin-top: 3px; }',
1403
+ '.cumulus-settings-body { padding: 6px 20px 14px; overflow-y: auto; flex: 1; }',
1334
1404
  '.cumulus-settings-footer {',
1335
- ' display: flex; align-items: center; gap: 12px; padding: 13px 18px;',
1336
- ' border-top: 1px solid #333; flex-shrink: 0;',
1405
+ ' display: flex; align-items: center; gap: 12px; padding: 13px 20px;',
1406
+ ' border-top: 1px solid #2a2b30; flex-shrink: 0; background: #1b1c1f;',
1337
1407
  '}',
1338
1408
  '.cumulus-settings-section-label {',
1339
- ' font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;',
1340
- ' color: #777; margin: 16px 0 8px;',
1409
+ ' font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;',
1410
+ ' color: #6e7278; margin: 18px 0 8px;',
1411
+ '}',
1412
+ '.cumulus-settings-body > .cumulus-settings-section-label:first-child { margin-top: 12px; }',
1413
+ '.cumulus-settings-default-card {',
1414
+ ' display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin: 4px 0 2px;',
1415
+ ' border: 1px solid rgba(79,140,255,0.35); border-radius: 12px; background: rgba(79,140,255,0.07);',
1341
1416
  '}',
1342
- '.cumulus-settings-body > .cumulus-settings-section-label:first-child { margin-top: 6px; }',
1417
+ '.cumulus-settings-default-card label { color: #c8d4e8; font-size: 12.5px; font-weight: 500; flex-shrink: 0; }',
1418
+ '.cumulus-settings-default-card select { flex: 1; min-width: 0; }',
1343
1419
  '.cumulus-settings-model-row {',
1344
- ' display: grid; grid-template-columns: auto auto minmax(130px,1.2fr) minmax(110px,1fr) 105px auto;',
1345
- ' align-items: center; gap: 8px; padding: 9px 11px;',
1346
- ' border: 1px solid #333; border-radius: 8px; margin-bottom: 7px; background: #1d1d1d;',
1420
+ ' display: flex; align-items: center; gap: 10px; padding: 9px 12px;',
1421
+ ' border: 1px solid #2c2d32; border-radius: 10px; margin-bottom: 6px; background: #1e1f23;',
1422
+ ' transition: border-color 0.12s;',
1347
1423
  '}',
1348
- '.cumulus-settings-model-row.anthropic { grid-template-columns: auto auto minmax(140px,1.2fr) minmax(120px,1fr) auto; }',
1349
- '.cumulus-settings-radio-label { color: #777; font-size: 9px; text-align: center; line-height: 1.15; }',
1424
+ '.cumulus-settings-model-row:hover { border-color: #3a3c42; }',
1350
1425
  '.cumulus-settings-row-input { min-width: 0; width: 100%; }',
1351
- '.cumulus-settings-model-row.is-default { border-color: #3b5b8c; background: rgba(59,91,140,0.12); }',
1352
- '.cumulus-settings-default-toggle { display: flex; align-items: center; cursor: pointer; flex-shrink: 0; }',
1353
- '.cumulus-settings-default-toggle input { cursor: pointer; accent-color: #3b82f6; margin: 0; }',
1426
+ '.cumulus-settings-model-row.is-default { border-color: rgba(79,140,255,0.45); background: rgba(79,140,255,0.08); }',
1354
1427
  '.cumulus-settings-model-name { flex: 1; min-width: 0; }',
1355
1428
  '.cumulus-settings-model-label {',
1356
- ' color: #e6e6e6; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
1429
+ ' color: #e7e8ea; font-size: 13px; font-weight: 500;',
1430
+ ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
1357
1431
  '}',
1358
1432
  '.cumulus-settings-model-id {',
1359
- ' color: #777; font-size: 11px; font-family: "SF Mono", Consolas, monospace;',
1433
+ ' color: #7d808a; font-size: 11px;',
1434
+ ' font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;',
1360
1435
  ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px;',
1361
1436
  '}',
1437
+ '.cumulus-settings-context-chip {',
1438
+ ' color: #9a9da5; font-size: 10.5px; font-variant-numeric: tabular-nums;',
1439
+ ' border: 1px solid #383a40; border-radius: 999px; background: #1b1c1f;',
1440
+ ' padding: 1px 8px; white-space: nowrap; flex-shrink: 0;',
1441
+ '}',
1362
1442
  '.cumulus-settings-default-badge {',
1363
- ' display: none; font-size: 10px; font-weight: 600; color: #8ab4f8;',
1364
- ' background: rgba(59,130,246,0.14); border: 1px solid rgba(59,130,246,0.35);',
1365
- ' border-radius: 10px; padding: 1px 8px; flex-shrink: 0;',
1443
+ ' display: none; font-size: 10.5px; font-weight: 600; color: #8ab4f8;',
1444
+ ' background: rgba(79,140,255,0.14); border: 1px solid rgba(79,140,255,0.35);',
1445
+ ' border-radius: 999px; padding: 1px 9px; flex-shrink: 0;',
1366
1446
  '}',
1367
1447
  '.cumulus-settings-model-row.is-default .cumulus-settings-default-badge { display: inline-block; }',
1448
+ '.cumulus-settings-row-actions {',
1449
+ ' display: flex; align-items: center; gap: 2px; flex-shrink: 0; opacity: 0;',
1450
+ ' transition: opacity 0.12s;',
1451
+ '}',
1452
+ '.cumulus-settings-model-row:hover .cumulus-settings-row-actions,',
1453
+ '.cumulus-settings-model-row:focus-within .cumulus-settings-row-actions { opacity: 1; }',
1454
+ '@media (hover: none) { .cumulus-settings-row-actions { opacity: 1; } }',
1455
+ '.cumulus-settings-row-edit {',
1456
+ ' background: none; border: none; color: #6e7278; cursor: pointer; font-size: 13px;',
1457
+ ' line-height: 1; padding: 2px 4px; flex-shrink: 0; font-family: inherit;',
1458
+ '}',
1459
+ '.cumulus-settings-row-edit:hover { color: #8ab4f8; }',
1368
1460
  '.cumulus-settings-model-remove {',
1369
- ' background: none; border: none; color: #666; cursor: pointer; font-size: 17px;',
1370
- ' line-height: 1; padding: 0 2px; flex-shrink: 0;',
1461
+ ' background: none; border: none; color: #6e7278; cursor: pointer; font-size: 17px;',
1462
+ ' line-height: 1; padding: 0 2px; flex-shrink: 0; font-family: inherit;',
1371
1463
  '}',
1372
1464
  '.cumulus-settings-model-remove:hover { color: #ef4444; }',
1465
+ '.cumulus-settings-model-row.editing { display: block; }',
1466
+ '.cumulus-settings-edit-grid {',
1467
+ ' display: grid; grid-template-columns: minmax(140px,1.3fr) minmax(110px,1fr) 105px auto;',
1468
+ ' gap: 8px; align-items: end;',
1469
+ '}',
1470
+ '.cumulus-settings-edit-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }',
1471
+ '.cumulus-settings-edit-field > span {',
1472
+ ' color: #6e7278; font-size: 10px; font-weight: 600;',
1473
+ ' text-transform: uppercase; letter-spacing: 0.05em;',
1474
+ '}',
1475
+ '.cumulus-settings-claude-default-row {',
1476
+ ' display: flex; align-items: center; gap: 10px; padding: 4px 12px 10px;',
1477
+ '}',
1478
+ '.cumulus-settings-claude-default-row label { color: #9a9da5; font-size: 11.5px; flex-shrink: 0; }',
1479
+ '.cumulus-settings-claude-default-row select { flex: 1; min-width: 0; }',
1373
1480
  '.cumulus-settings-provider-card {',
1374
- ' border: 1px solid #363636; border-radius: 10px; background: #1d1d1d; margin: 10px 0;',
1481
+ ' border: 1px solid #2c2d32; border-radius: 12px; background: #232428; margin: 10px 0;',
1375
1482
  ' overflow: hidden;',
1376
1483
  '}',
1377
1484
  '.cumulus-settings-provider-head {',
1378
1485
  ' display: flex; align-items: center; justify-content: space-between; gap: 14px;',
1379
- ' padding: 12px 13px; background: #202020; border-bottom: 1px solid #323232;',
1486
+ ' padding: 13px 14px; background: #26272c; border-bottom: 1px solid #2c2d32;',
1380
1487
  '}',
1381
- '.cumulus-settings-provider-title { color: #ededed; font-size: 14px; font-weight: 650; }',
1382
- '.cumulus-settings-provider-desc { color: #777; font-size: 11px; margin-top: 2px; }',
1488
+ '.cumulus-settings-provider-title { color: #f2f3f5; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }',
1489
+ '.cumulus-settings-provider-desc { color: #7d808a; font-size: 11.5px; margin-top: 2px; }',
1383
1490
  '.cumulus-settings-provider-count {',
1384
- ' color: #999; font-size: 10px; border: 1px solid #3c3c3c; border-radius: 999px;',
1385
- ' padding: 2px 8px; white-space: nowrap;',
1491
+ ' color: #9a9da5; font-size: 10.5px; font-variant-numeric: tabular-nums;',
1492
+ ' border: 1px solid #383a40; border-radius: 999px; background: #1b1c1f;',
1493
+ ' padding: 2px 9px; white-space: nowrap;',
1386
1494
  '}',
1387
- '.cumulus-settings-provider-models { padding: 10px 10px 3px; }',
1388
- '.cumulus-settings-provider-models .cumulus-settings-model-row { margin-bottom: 7px; }',
1389
- '.cumulus-settings-provider-empty { color: #666; font-size: 12px; padding: 8px 3px 15px; }',
1495
+ '.cumulus-settings-provider-models { padding: 10px 10px 4px; }',
1496
+ '.cumulus-settings-provider-models .cumulus-settings-model-row { margin-bottom: 6px; }',
1497
+ '.cumulus-settings-provider-empty { color: #6e7278; font-size: 12px; padding: 8px 4px 14px; }',
1390
1498
  '.cumulus-settings-provider-credential {',
1391
1499
  ' display: grid; grid-template-columns: 120px minmax(0,1fr) auto; align-items: center; gap: 10px;',
1392
- ' padding: 0 10px 11px;',
1500
+ ' padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1501
+ '}',
1502
+ '.cumulus-settings-credential-note {',
1503
+ ' color: #7d808a; font-size: 11.5px; padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1393
1504
  '}',
1394
- '.cumulus-settings-credential-note { color: #777; font-size: 11px; padding: 0 10px 12px; }',
1395
1505
  '.cumulus-settings-clear-btn {',
1396
- ' background: none; border: 1px solid #454545; color: #aaa; cursor: pointer;',
1397
- ' padding: 6px 9px; border-radius: 6px; font-size: 11px;',
1506
+ ' background: transparent; border: 1px solid #3d3f45; color: #9a9da5; cursor: pointer;',
1507
+ ' padding: 6px 10px; border-radius: 8px; font-size: 11.5px; font-family: inherit;',
1508
+ ' transition: color 0.12s, border-color 0.12s;',
1398
1509
  '}',
1399
- '.cumulus-settings-clear-btn:hover { color: #ef7777; border-color: #744; }',
1400
- '.cumulus-settings-provider-credential label { color: #aaa; font-size: 11px; }',
1510
+ '.cumulus-settings-clear-btn:hover { color: #ef7777; border-color: #7a4444; }',
1511
+ '.cumulus-settings-provider-credential label { color: #9a9da5; font-size: 11.5px; }',
1401
1512
  '.cumulus-settings-add-card {',
1402
- ' border: 1px dashed #454545; border-radius: 10px; padding: 12px; margin: 12px 0 8px;',
1513
+ ' border: 1px dashed #3d3f45; border-radius: 12px; padding: 13px 14px; margin: 12px 0 8px;',
1403
1514
  ' background: rgba(255,255,255,0.015);',
1404
1515
  '}',
1405
- '.cumulus-settings-add-title { color: #ddd; font-size: 12px; font-weight: 650; margin-bottom: 9px; }',
1516
+ '.cumulus-settings-add-title { color: #d4d6da; font-size: 12px; font-weight: 600; margin-bottom: 9px; }',
1406
1517
  '.cumulus-settings-add-row {',
1407
1518
  ' display: grid; grid-template-columns: 130px minmax(150px,1.4fr) minmax(120px,1fr) 110px auto;',
1408
- ' gap: 7px; align-items: center;',
1519
+ ' gap: 8px; align-items: center;',
1520
+ '}',
1521
+ '.cumulus-settings-select, .cumulus-settings-input {',
1522
+ ' background: #17181b; border: 1px solid #33353b; color: #e3e5ea;',
1523
+ ' padding: 7px 10px; border-radius: 8px; font-size: 12.5px; font-family: inherit; outline: none;',
1524
+ ' transition: border-color 0.12s, box-shadow 0.12s;',
1409
1525
  '}',
1410
1526
  '.cumulus-settings-select {',
1411
- ' background: #1a1a1a; border: 1px solid #3a3a3a; color: #e0e0e0;',
1412
- ' padding: 7px 9px; border-radius: 6px; font-size: 12px; font-family: inherit; outline: none;',
1527
+ ' -webkit-appearance: none; appearance: none; padding-right: 26px; cursor: pointer;',
1528
+ ' background-image: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2710%27 height=%276%27%3E%3Cpath d=%27M1 1l4 4 4-4%27 stroke=%27%238b8f96%27 stroke-width=%271.5%27 fill=%27none%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E");',
1529
+ ' background-repeat: no-repeat; background-position: right 10px center;',
1413
1530
  '}',
1414
- '.cumulus-settings-input {',
1415
- ' background: #1a1a1a; border: 1px solid #3a3a3a; color: #e0e0e0;',
1416
- ' padding: 7px 9px; border-radius: 6px; font-size: 12px; font-family: inherit; outline: none;',
1531
+ '.cumulus-settings-select:focus, .cumulus-settings-input:focus {',
1532
+ ' border-color: #4f8cff; box-shadow: 0 0 0 3px rgba(79,140,255,0.18);',
1417
1533
  '}',
1418
- '.cumulus-settings-input:focus { border-color: #3b82f6; }',
1419
- '.cumulus-settings-input::placeholder { color: #5a5a5a; }',
1534
+ '.cumulus-settings-input::placeholder { color: #5c5f66; }',
1420
1535
  '.cumulus-settings-cred-row { display: flex; align-items: center; gap: 12px; }',
1421
- '.cumulus-settings-cred-row label { font-size: 12px; color: #bbb; flex-shrink: 0; }',
1536
+ '.cumulus-settings-cred-row label { font-size: 12px; color: #b8bac0; flex-shrink: 0; }',
1422
1537
  '.cumulus-settings-btn {',
1423
- ' background: #2a6acf; border: none; color: #fff; font-weight: 600;',
1424
- ' padding: 8px 18px; border-radius: 6px; font-size: 13px; cursor: pointer; font-family: inherit;',
1538
+ ' background: #3b82f6; border: none; color: #fff; font-weight: 600;',
1539
+ ' padding: 8px 20px; border-radius: 8px; font-size: 13px; cursor: pointer; font-family: inherit;',
1540
+ ' transition: background 0.12s;',
1425
1541
  '}',
1426
- '.cumulus-settings-btn:hover { background: #3a7ae0; }',
1427
- '.cumulus-settings-btn:disabled { background: #333; color: #777; cursor: not-allowed; }',
1542
+ '.cumulus-settings-btn:hover { background: #2f6fe0; }',
1543
+ '.cumulus-settings-btn:disabled { background: #2c2d32; color: #6e7278; cursor: not-allowed; }',
1428
1544
  '.cumulus-settings-add-btn {',
1429
- ' background: #2f2f2f; border: 1px solid #454545; color: #ddd;',
1430
- ' padding: 7px 12px; border-radius: 6px; font-size: 12px; cursor: pointer;',
1431
- ' font-family: inherit; white-space: nowrap;',
1545
+ ' background: transparent; border: 1px solid #3d3f45; color: #d4d6da;',
1546
+ ' padding: 7px 14px; border-radius: 8px; font-size: 12.5px; cursor: pointer;',
1547
+ ' font-family: inherit; white-space: nowrap; transition: background 0.12s, border-color 0.12s;',
1432
1548
  '}',
1433
- '.cumulus-settings-add-btn:hover { background: #3a3a3a; border-color: #555; }',
1434
- '.cumulus-settings-status { font-size: 12px; color: #999; }',
1549
+ '.cumulus-settings-add-btn:hover { background: #2a2b30; border-color: #4a4c53; }',
1550
+ '.cumulus-settings-status { font-size: 12px; color: #9a9da5; }',
1435
1551
  '.cumulus-settings-status.success { color: #22c55e; }',
1436
1552
  '.cumulus-settings-status.error { color: #ef4444; }',
1437
1553
  '@media (max-width: 720px) {',
1438
1554
  ' .cumulus-settings-backdrop { padding: 8px; align-items: stretch; }',
1439
- ' .cumulus-settings-modal { max-height: 100%; border-radius: 8px; }',
1555
+ ' .cumulus-settings-modal { max-height: 100%; border-radius: 12px; }',
1440
1556
  ' .cumulus-settings-add-row { grid-template-columns: 1fr 1fr; }',
1441
1557
  ' .cumulus-settings-add-row .cumulus-settings-add-btn { grid-column: 1 / -1; }',
1442
1558
  ' .cumulus-settings-provider-credential { grid-template-columns: 1fr auto; gap: 5px; }',
1443
1559
  ' .cumulus-settings-provider-credential label { grid-column: 1 / -1; }',
1444
- ' .cumulus-settings-model-row, .cumulus-settings-model-row.anthropic {',
1445
- ' grid-template-columns: auto auto 1fr auto;',
1446
- ' }',
1447
- ' .cumulus-settings-model-row .cumulus-settings-row-input { grid-column: 3 / 4; }',
1448
- ' .cumulus-settings-model-row .cumulus-settings-model-remove { grid-column: 4; grid-row: 1 / 3; }',
1560
+ ' .cumulus-settings-default-card { flex-direction: column; align-items: stretch; gap: 6px; }',
1561
+ ' .cumulus-settings-edit-grid { grid-template-columns: 1fr 1fr; }',
1449
1562
  '}',
1450
1563
 
1451
1564
  /* ── Context menu (right-click on thread) ── */
@@ -1937,8 +2050,8 @@
1937
2050
 
1938
2051
  // ── Blex Integration ─────────────────────────────────────────────────────
1939
2052
 
1940
- // WeakMap: DOM element -> BlockHandle[] for lifecycle management
1941
- var blexHandles = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
2053
+ // Handle lifecycle (element -> BlockHandle[]) lives in blex-render.js — one
2054
+ // owner, so destroy() cannot miss handles the other side created.
1942
2055
 
1943
2056
  // Per-thread store for blex interaction values (poll answers, confirm clicks, etc.)
1944
2057
  // Key: "threadName" → Map<"msgTimestamp:blockIdx", interactionValue>
@@ -2089,136 +2202,99 @@
2089
2202
  });
2090
2203
  }
2091
2204
 
2092
- // Extract ~~~blex:TYPE\n{json}\n~~~ fences from text
2093
- // Returns { text: string (with placeholders), blocks: Array<{type, json, idx}> }
2205
+ // ── Shared renderer seam (task 131) ────────────────────────────────────────
2206
+ //
2207
+ // Extraction, placeholder markup, block rendering and handle lifecycle live in
2208
+ // /blex-render.js — ONE implementation shared with the web-app-agent kit and
2209
+ // any embedding app. chat.html loads it before this file.
2210
+ //
2211
+ // The EMBEDDED widget (a bare <script src="/widget.js"> on a third-party page)
2212
+ // loads neither blex.min.js nor blex-render.js, and never has — blex has only
2213
+ // ever worked on /chat. There, fences degrade to literal text, which is what
2214
+ // an author who typed one would expect to see.
2215
+ function blexCore() {
2216
+ return typeof CumulusBlexRender !== 'undefined' ? CumulusBlexRender : null;
2217
+ }
2218
+
2219
+ // Fenced-code regex, sourced from the shared module so the blex extractor and
2220
+ // Phase 1 of renderMarkdown cannot disagree about where a code block starts —
2221
+ // a fence falling through that gap is exactly the bug this seam closes.
2222
+ function codeFenceRe() {
2223
+ var core = blexCore();
2224
+ return new RegExp(core ? core.CODE_FENCE_SRC : '```([^\\n`]*)\\n([\\s\\S]*?)```', 'g');
2225
+ }
2226
+
2094
2227
  function extractBlexBlocks(text) {
2095
- var blocks = [];
2096
- var BLEX_PREFIX = '\x00BLEX_';
2097
- var replaced = text.replace(/~~~blex:(\w[\w-]*)\n([\s\S]*?)\n~~~/g, function (_, type, json) {
2098
- var idx = blocks.length;
2099
- blocks.push({ type: type, json: json.trim(), idx: idx });
2100
- return BLEX_PREFIX + idx + '\x00';
2101
- });
2102
- return { text: replaced, blocks: blocks };
2228
+ var core = blexCore();
2229
+ return core ? core.extractBlocks(text) : { text: text, blocks: [] };
2103
2230
  }
2104
2231
 
2105
- // Render blex blocks into placeholder divs within a message element
2106
- function renderBlexBlocks(el, blexBlocks, isStreaming, msgKey, threadName) {
2107
- if (!blexBlocks || blexBlocks.length === 0) return;
2108
- if (typeof Blex === 'undefined') {
2109
- console.warn('[blex] Blex library not loaded blex.min.js may have failed to fetch');
2110
- return;
2111
- }
2112
- var placeholders = el.querySelectorAll('.blex-block-container');
2113
- // Get the interaction store for this thread
2232
+ // The widget's adapter over the shared renderer. It supplies all five members;
2233
+ // a render-only surface (the web-app-agent kit's panel) supplies the three
2234
+ // required ones and lets addChip/persist default to no-ops.
2235
+ //
2236
+ // `el` is captured because it may be DETACHED by the time an interaction fires
2237
+ // — renderPanelMessages rebuilds the message list — which is why the panel
2238
+ // lookup falls back to a document query.
2239
+ function widgetBlexAdapter(el, threadName) {
2114
2240
  var store = threadName
2115
2241
  ? blexInteractionStore[threadName] || (blexInteractionStore[threadName] = {})
2116
2242
  : null;
2117
2243
 
2118
- placeholders.forEach(function (container) {
2119
- var idx = parseInt(container.getAttribute('data-blex-idx'), 10);
2120
- if (isNaN(idx) || idx >= blexBlocks.length) return;
2121
- var block = blexBlocks[idx];
2122
- var storeKey = msgKey ? msgKey + ':' + idx : null;
2123
-
2124
- try {
2125
- var data = JSON.parse(block.json);
2126
- var blockObj = { type: block.type, id: block.type + '-' + idx, data: data };
2127
-
2128
- if (isStreaming) {
2129
- // During streaming, show placeholder skeleton
2130
- Blex.renderPlaceholder(block.type, container);
2131
- } else {
2132
- // Check for previously stored interaction value
2133
- var renderOpts = {};
2134
- if (store && storeKey && store[storeKey]) {
2135
- renderOpts.previousValue = store[storeKey];
2136
- }
2244
+ function panelFor() {
2245
+ return (
2246
+ el.closest('.cumulus-thread-panel') ||
2247
+ el.closest('.cumulus-panel') ||
2248
+ document.querySelector('.cumulus-thread-panel') ||
2249
+ document.querySelector('.cumulus-panel')
2250
+ );
2251
+ }
2137
2252
 
2138
- // Final render — full interactive block
2139
- Blex.renderBlock(blockObj, container, renderOpts)
2140
- .then(function (handle) {
2141
- // Store handle immediately (inside async callback, not after sync loop)
2142
- if (blexHandles) {
2143
- var existing = blexHandles.get(el) || [];
2144
- existing.push(handle);
2145
- blexHandles.set(el, existing);
2146
- }
2147
- // Attach resize handle (drag to resize, double-click to reset)
2148
- attachBlexResize(container, threadName, storeKey);
2149
- if (handle && handle.onInteraction) {
2150
- // Wire interaction handler
2151
- handle.onInteraction(function (interaction) {
2152
- // Store the interaction value for persistence across re-renders + refreshes
2153
- if (store && storeKey && interaction.value !== undefined) {
2154
- store[storeKey] = interaction.value;
2155
- _flushBlexStore();
2156
- }
2253
+ return {
2254
+ // The widget is the interactive surface: every type is allowed here.
2255
+ allowType: function () {
2256
+ return true;
2257
+ },
2258
+ getInput: function () {
2259
+ var panel = panelFor();
2260
+ return panel
2261
+ ? panel.querySelector('[data-testid="webchat-input"]')
2262
+ : document.querySelector('[data-testid="webchat-input"]');
2263
+ },
2264
+ getSendButton: function () {
2265
+ var panel = panelFor();
2266
+ return panel ? panel.querySelector('[data-testid*="send"]') : null;
2267
+ },
2268
+ addChip: function (interaction, handle, sourceId) {
2269
+ addInteractionChip(panelFor(), interaction, handle, sourceId);
2270
+ },
2271
+ readPersisted: function (storeKey) {
2272
+ if (!store || !storeKey) return undefined;
2273
+ return store[storeKey] || undefined;
2274
+ },
2275
+ persist: function (storeKey, value) {
2276
+ if (!store || !storeKey) return;
2277
+ store[storeKey] = value;
2278
+ _flushBlexStore();
2279
+ },
2280
+ onRendered: function (container, storeKey) {
2281
+ attachBlexResize(container, threadName, storeKey);
2282
+ },
2283
+ };
2284
+ }
2157
2285
 
2158
- // Find the panel — try el.closest first, fall back to document query
2159
- // (el may be detached from DOM if renderPanelMessages rebuilt the message list)
2160
- var panel = el.closest('.cumulus-thread-panel') || el.closest('.cumulus-panel');
2161
- if (!panel) {
2162
- panel =
2163
- document.querySelector('.cumulus-thread-panel') ||
2164
- document.querySelector('.cumulus-panel');
2165
- }
2166
- var inputEl = panel
2167
- ? panel.querySelector('[data-testid="webchat-input"]')
2168
- : document.querySelector('[data-testid="webchat-input"]');
2169
-
2170
- if (inputEl && interaction.serialized) {
2171
- if (interaction.immediate) {
2172
- // Immediate: prepend any existing input text, then auto-send
2173
- var existingText = inputEl.value.trim();
2174
- inputEl.value = existingText
2175
- ? existingText + '\n\n' + interaction.serialized
2176
- : interaction.serialized;
2177
- inputEl.dispatchEvent(new Event('input', { bubbles: true }));
2178
- // Find and click the send button
2179
- var sendBtn = panel ? panel.querySelector('[data-testid*="send"]') : null;
2180
- if (sendBtn) {
2181
- sendBtn.click();
2182
- }
2183
- } else {
2184
- // Deferred: add to chip tray (sourceId deduplicates rapid-fire from same block)
2185
- addInteractionChip(panel, interaction, handle, block.type + '-' + idx);
2186
- }
2187
- }
2188
- });
2189
- }
2190
- })
2191
- .catch(function (err) {
2192
- container.innerHTML =
2193
- '<pre style="color:#f66;font-size:0.85em;padding:0.5em;">Blex error: ' +
2194
- (err.message || err) +
2195
- '</pre>';
2196
- });
2197
- }
2198
- } catch (e) {
2199
- container.innerHTML =
2200
- '<pre style="color:#f66;font-size:0.85em;padding:0.5em;">Invalid blex JSON: ' +
2201
- e.message +
2202
- '</pre>';
2203
- }
2286
+ function renderBlexBlocks(el, blexBlocks, isStreaming, msgKey, threadName) {
2287
+ var core = blexCore();
2288
+ if (!core) return;
2289
+ core.render(el, blexBlocks, widgetBlexAdapter(el, threadName), {
2290
+ msgKey: msgKey,
2291
+ streaming: isStreaming,
2204
2292
  });
2205
-
2206
- // Note: handles are stored inside the async .then() callback above,
2207
- // not here — by this point the promises haven't resolved yet.
2208
2293
  }
2209
2294
 
2210
- // Destroy blex block handles for a message element
2211
2295
  function destroyBlexBlocks(el) {
2212
- if (!blexHandles) return;
2213
- var handles = blexHandles.get(el);
2214
- if (handles) {
2215
- handles.forEach(function (h) {
2216
- try {
2217
- h.destroy();
2218
- } catch (e) {}
2219
- });
2220
- blexHandles.delete(el);
2221
- }
2296
+ var core = blexCore();
2297
+ if (core) core.destroy(el);
2222
2298
  }
2223
2299
 
2224
2300
  // Add a deferred interaction chip to the chip tray
@@ -2581,7 +2657,12 @@
2581
2657
  return out;
2582
2658
  }
2583
2659
 
2584
- function renderMarkdown(text) {
2660
+ // opts.streaming — suppress the raw-fence fallback body while a turn is still
2661
+ // streaming. A completed-but-not-yet-rendered fence would otherwise flash its
2662
+ // JSON before flipping to the rendered block at `done`.
2663
+ function renderMarkdown(text, opts) {
2664
+ var streaming = !!(opts && opts.streaming);
2665
+
2585
2666
  // Phase 0.5: Extract blex fences BEFORE code blocks
2586
2667
  var blexResult = extractBlexBlocks(text);
2587
2668
  text = blexResult.text;
@@ -2590,7 +2671,7 @@
2590
2671
  // Phase 1: Extract fenced code blocks — replace with unique tokens
2591
2672
  var codeBlocks = [];
2592
2673
  var TOKEN_PREFIX = '\x00CODEBLOCK_';
2593
- var processed = text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, function (_, lang, code) {
2674
+ var processed = text.replace(codeFenceRe(), function (_, lang, code) {
2594
2675
  var idx = codeBlocks.length;
2595
2676
  var langLabel = lang.trim() || 'text';
2596
2677
  var escapedCode = escapeHtml(code.replace(/\n$/, '')); // trim trailing newline
@@ -2667,10 +2748,14 @@
2667
2748
  return codeBlocks[parseInt(idx, 10)];
2668
2749
  });
2669
2750
 
2670
- // Phase 7.5: Reinsert blex block placeholders
2671
- html = html.replace(/\x00BLEX_(\d+)\x00/g, function (_, idx) {
2672
- return '<div class="blex-block-container" data-blex-idx="' + idx + '"></div>';
2673
- });
2751
+ // Phase 7.5: Reinsert blex block placeholders — shared markup, so the widget
2752
+ // and any embedding app produce byte-identical containers and one renderer
2753
+ // can claim either. See blex-render.js for why the placeholder carries the
2754
+ // raw fence as its body.
2755
+ var core = blexCore();
2756
+ if (core) {
2757
+ html = core.insertPlaceholders(html, blexBlocks, { streaming: streaming });
2758
+ }
2674
2759
 
2675
2760
  return { html: html, blexBlocks: blexBlocks };
2676
2761
  }
@@ -3660,7 +3745,7 @@
3660
3745
  el.className = 'cumulus-msg assistant';
3661
3746
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
3662
3747
  if (content) {
3663
- var mdResult = renderMarkdown(content);
3748
+ var mdResult = renderMarkdown(content, { streaming: isStreaming });
3664
3749
  el.innerHTML = mdResult.html;
3665
3750
  if (isStreaming) {
3666
3751
  el.innerHTML += '<span class="cumulus-cursor"></span>';
@@ -4500,6 +4585,7 @@
4500
4585
  '<button class="cumulus-topbar-check-update" data-testid="webchat-check-update" title="Check for update">&#8635;</button>' +
4501
4586
  '<span class="cumulus-topbar-check-result" data-testid="webchat-check-result" style="display:none"></span></span>' +
4502
4587
  '<div class="cumulus-topbar-right">' +
4588
+ '<button class="cumulus-topbar-settings" data-testid="webchat-settings-btn" title="Gateway settings — models, providers, API keys">&#9881;</button>' +
4503
4589
  '<span class="cumulus-header-status">' +
4504
4590
  '<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
4505
4591
  '<span data-testid="webchat-status-text-app">Disconnected</span>' +
@@ -4689,6 +4775,14 @@
4689
4775
  });
4690
4776
  }
4691
4777
 
4778
+ // ── Topbar settings gear → gateway settings modal (task 128) ──
4779
+ var topbarSettingsBtn = topbar.querySelector('[data-testid="webchat-settings-btn"]');
4780
+ if (topbarSettingsBtn) {
4781
+ topbarSettingsBtn.addEventListener('click', function () {
4782
+ openSettingsModal();
4783
+ });
4784
+ }
4785
+
4692
4786
  // ── Click status indicator to force reconnect ──
4693
4787
  var appStatusEl = topbar.querySelector('.cumulus-header-status');
4694
4788
  if (appStatusEl) {
@@ -5846,6 +5940,715 @@
5846
5940
  };
5847
5941
  }
5848
5942
 
5943
+ // ── Settings modal (provider-organized model catalog + credentials) ──
5944
+ // Live-editable via PUT /api/config (task 104) — takes effect on the NEXT turn
5945
+ // with no restart, no JSON edit, no republish. Any valid gateway key.
5946
+ // Gateway-level config, opened from the topbar gear next to the connection
5947
+ // status — not tied to any thread panel (task 128).
5948
+ function openSettingsModal() {
5949
+ var backdrop = document.createElement('div');
5950
+ backdrop.className = 'cumulus-settings-backdrop';
5951
+ backdrop.setAttribute('data-testid', 'webchat-settings-modal');
5952
+ var modal = document.createElement('div');
5953
+ modal.className = 'cumulus-settings-modal';
5954
+
5955
+ var mHeader = document.createElement('div');
5956
+ mHeader.className = 'cumulus-settings-header';
5957
+ var mHeaderText = document.createElement('div');
5958
+ var mTitle = document.createElement('div');
5959
+ mTitle.className = 'cumulus-settings-title';
5960
+ mTitle.textContent = 'Gateway settings';
5961
+ var mSub = document.createElement('div');
5962
+ mSub.className = 'cumulus-settings-subtitle';
5963
+ mSub.textContent =
5964
+ 'Manage Anthropic, OpenAI, and HuggingFace models — changes apply to new turns instantly';
5965
+ mHeaderText.appendChild(mTitle);
5966
+ mHeaderText.appendChild(mSub);
5967
+ var mClose = document.createElement('button');
5968
+ mClose.className = 'cumulus-overlay-close';
5969
+ mClose.textContent = '\xD7';
5970
+ mClose.setAttribute('data-testid', 'webchat-settings-close');
5971
+ mClose.addEventListener('click', function () {
5972
+ backdrop.remove();
5973
+ });
5974
+ mHeader.appendChild(mHeaderText);
5975
+ mHeader.appendChild(mClose);
5976
+ modal.appendChild(mHeader);
5977
+
5978
+ var body = document.createElement('div');
5979
+ body.className = 'cumulus-settings-body';
5980
+
5981
+ // Task 130 (Option C): ONE gateway-default control, up top — answers "what
5982
+ // will a new thread use?" before the catalog. The per-row radios are gone.
5983
+ var defaultLabelSection = document.createElement('div');
5984
+ defaultLabelSection.className = 'cumulus-settings-section-label';
5985
+ defaultLabelSection.textContent = 'Default model';
5986
+ body.appendChild(defaultLabelSection);
5987
+ var defaultCard = document.createElement('div');
5988
+ defaultCard.className = 'cumulus-settings-default-card';
5989
+ var defaultCardLabel = document.createElement('label');
5990
+ defaultCardLabel.textContent = 'New threads use';
5991
+ var defaultSelect = document.createElement('select');
5992
+ defaultSelect.className = 'cumulus-settings-select';
5993
+ defaultSelect.setAttribute('data-testid', 'webchat-gw-default-select');
5994
+ defaultSelect.addEventListener('change', function () {
5995
+ applyGatewayDefault(gwState.catalog, defaultSelect.value);
5996
+ renderAll();
5997
+ });
5998
+ defaultCard.appendChild(defaultCardLabel);
5999
+ defaultCard.appendChild(defaultSelect);
6000
+ body.appendChild(defaultCard);
6001
+
6002
+ var modelsLabel = document.createElement('div');
6003
+ modelsLabel.className = 'cumulus-settings-section-label';
6004
+ modelsLabel.textContent = 'Model catalog';
6005
+ body.appendChild(modelsLabel);
6006
+
6007
+ var gwList = document.createElement('div');
6008
+ gwList.setAttribute('data-testid', 'webchat-gateway-models');
6009
+ body.appendChild(gwList);
6010
+
6011
+ var addCard = document.createElement('div');
6012
+ addCard.className = 'cumulus-settings-add-card';
6013
+ var addTitle = document.createElement('div');
6014
+ addTitle.className = 'cumulus-settings-add-title';
6015
+ addTitle.textContent = 'Add a model';
6016
+ addCard.appendChild(addTitle);
6017
+ var addWrap = document.createElement('div');
6018
+ addWrap.className = 'cumulus-settings-add-row';
6019
+ var addProvider = document.createElement('select');
6020
+ addProvider.className = 'cumulus-settings-select';
6021
+ addProvider.setAttribute('data-testid', 'webchat-gw-add-provider');
6022
+ [
6023
+ { value: 'anthropic', label: 'Anthropic' },
6024
+ { value: 'openai', label: 'OpenAI' },
6025
+ { value: 'huggingface', label: 'HuggingFace' },
6026
+ ].forEach(function (provider) {
6027
+ var option = document.createElement('option');
6028
+ option.value = provider.value;
6029
+ option.textContent = provider.label;
6030
+ addProvider.appendChild(option);
6031
+ });
6032
+ var addId = document.createElement('input');
6033
+ addId.type = 'text';
6034
+ addId.className = 'cumulus-settings-input';
6035
+ addId.placeholder = 'Model ID / name';
6036
+ addId.setAttribute('data-testid', 'webchat-gw-add-id');
6037
+ var addLabel = document.createElement('input');
6038
+ addLabel.type = 'text';
6039
+ addLabel.className = 'cumulus-settings-input';
6040
+ addLabel.placeholder = 'Display label (optional)';
6041
+ addLabel.setAttribute('data-testid', 'webchat-gw-add-label');
6042
+ var addContext = document.createElement('input');
6043
+ addContext.type = 'number';
6044
+ addContext.min = '1';
6045
+ addContext.step = '1';
6046
+ addContext.className = 'cumulus-settings-input';
6047
+ addContext.placeholder = 'Context tokens';
6048
+ addContext.setAttribute('data-testid', 'webchat-gw-add-context');
6049
+ var addBtn = document.createElement('button');
6050
+ addBtn.type = 'button';
6051
+ addBtn.textContent = '+ Add';
6052
+ addBtn.className = 'cumulus-settings-add-btn';
6053
+ addBtn.setAttribute('data-testid', 'webchat-gw-add-btn');
6054
+ addWrap.appendChild(addProvider);
6055
+ addWrap.appendChild(addId);
6056
+ addWrap.appendChild(addLabel);
6057
+ addWrap.appendChild(addContext);
6058
+ addWrap.appendChild(addBtn);
6059
+ addCard.appendChild(addWrap);
6060
+ body.appendChild(addCard);
6061
+
6062
+ // ── License (task 129) — persist-only: the key lands in gateway.config.json
6063
+ // but enforcement re-verifies at startup, so it takes effect on reload (127).
6064
+ var licenseLabel = document.createElement('div');
6065
+ licenseLabel.className = 'cumulus-settings-section-label';
6066
+ licenseLabel.textContent = 'License';
6067
+ body.appendChild(licenseLabel);
6068
+ var licenseCard = document.createElement('div');
6069
+ licenseCard.className = 'cumulus-settings-add-card';
6070
+ var licenseStatusLine = document.createElement('div');
6071
+ licenseStatusLine.className = 'cumulus-settings-add-title';
6072
+ licenseStatusLine.setAttribute('data-testid', 'webchat-gw-license-status');
6073
+ licenseStatusLine.textContent = 'License status unknown';
6074
+ licenseCard.appendChild(licenseStatusLine);
6075
+ var licensePurpose = document.createElement('div');
6076
+ licensePurpose.className = 'cumulus-settings-subtitle';
6077
+ licensePurpose.setAttribute('data-testid', 'webchat-gw-license-purpose');
6078
+ licensePurpose.textContent =
6079
+ 'Required to run web-app agents in production. Unlicensed, each thread ' +
6080
+ 'namespace is capped at 5 visitor threads (demo mode); your own threads, ' +
6081
+ 'the CLI and the TUI are never limited. Keys are verified offline and ' +
6082
+ 'cover one whole-number release. Contact ops@luckydrawdesign.com.';
6083
+ licenseCard.appendChild(licensePurpose);
6084
+ var licenseRow = document.createElement('div');
6085
+ licenseRow.className = 'cumulus-settings-add-row';
6086
+ var licenseInput = document.createElement('input');
6087
+ licenseInput.type = 'text';
6088
+ licenseInput.className = 'cumulus-settings-input';
6089
+ licenseInput.placeholder = 'License key (cumulus-lic-v1.…)';
6090
+ licenseInput.setAttribute('data-testid', 'webchat-gw-license-input');
6091
+ var licenseClearBtn = document.createElement('button');
6092
+ licenseClearBtn.type = 'button';
6093
+ licenseClearBtn.textContent = 'Clear';
6094
+ licenseClearBtn.className = 'cumulus-settings-add-btn';
6095
+ licenseClearBtn.setAttribute('data-testid', 'webchat-gw-license-clear');
6096
+ licenseRow.appendChild(licenseInput);
6097
+ licenseRow.appendChild(licenseClearBtn);
6098
+ licenseCard.appendChild(licenseRow);
6099
+ var licenseNote = document.createElement('div');
6100
+ licenseNote.className = 'cumulus-settings-subtitle';
6101
+ licenseNote.textContent = 'Takes effect on gateway reload';
6102
+ licenseCard.appendChild(licenseNote);
6103
+ body.appendChild(licenseCard);
6104
+ modal.appendChild(body);
6105
+
6106
+ var footer = document.createElement('div');
6107
+ footer.className = 'cumulus-settings-footer';
6108
+ var saveBtn = document.createElement('button');
6109
+ saveBtn.textContent = 'Save';
6110
+ saveBtn.className = 'cumulus-settings-btn';
6111
+ saveBtn.setAttribute('data-testid', 'webchat-gw-save');
6112
+ var gwStatus = document.createElement('span');
6113
+ gwStatus.className = 'cumulus-settings-status';
6114
+ footer.appendChild(saveBtn);
6115
+ footer.appendChild(gwStatus);
6116
+ modal.appendChild(footer);
6117
+
6118
+ backdrop.appendChild(modal);
6119
+ backdrop.addEventListener('click', function (e) {
6120
+ if (e.target === backdrop) backdrop.remove();
6121
+ });
6122
+ document.body.appendChild(backdrop);
6123
+
6124
+ var gwState = { catalog: [], sentinel: null };
6125
+ var credentialInputs = {};
6126
+ var credentialValues = { openai: '', huggingface: '', license: '' };
6127
+ var credentialClears = { openai: false, huggingface: false, license: false };
6128
+ function renderLicenseState(masked, info) {
6129
+ licenseInput.value = credentialValues.license || '';
6130
+ licenseInput.placeholder = credentialClears.license
6131
+ ? 'Will be cleared on Save'
6132
+ : masked || 'License key (cumulus-lic-v1.…)';
6133
+ if (!info) {
6134
+ licenseStatusLine.textContent = masked
6135
+ ? 'License key configured'
6136
+ : 'No license key — demo mode';
6137
+ return;
6138
+ }
6139
+ var text = info.licensed
6140
+ ? 'Licensed to ' + (info.licensee || 'unknown')
6141
+ : 'Demo mode' + (info.reason && info.reason !== 'absent' ? ' (' + info.reason + ')' : '');
6142
+ if (info.pendingReload) text += ' · key saved — takes effect on reload';
6143
+ licenseStatusLine.textContent = text;
6144
+ }
6145
+ licenseInput.addEventListener('input', function () {
6146
+ credentialValues.license = licenseInput.value;
6147
+ credentialClears.license = false;
6148
+ });
6149
+ licenseClearBtn.addEventListener('click', function () {
6150
+ credentialClears.license = true;
6151
+ credentialValues.license = '';
6152
+ renderLicenseState('', gwLicenseInfo);
6153
+ });
6154
+ var gwLicenseMasked = '';
6155
+ var gwLicenseInfo = null;
6156
+ var providerDefs = [
6157
+ {
6158
+ id: 'anthropic',
6159
+ title: 'Anthropic',
6160
+ description: 'Claude CLI models · authentication is managed by the Claude CLI',
6161
+ },
6162
+ {
6163
+ id: 'openai',
6164
+ title: 'OpenAI',
6165
+ description: 'Responses API models with direct tool use',
6166
+ credential: 'openaiApiKey',
6167
+ credentialTestId: 'webchat-gw-openaikey',
6168
+ },
6169
+ {
6170
+ id: 'huggingface',
6171
+ title: 'HuggingFace',
6172
+ description: 'OpenAI-compatible models served through the HuggingFace router',
6173
+ credential: 'hfApiKey',
6174
+ credentialTestId: 'webchat-gw-hfkey',
6175
+ },
6176
+ ];
6177
+
6178
+ function setSettingsStatus(message, kind) {
6179
+ gwStatus.textContent = message || '';
6180
+ gwStatus.className = 'cumulus-settings-status' + (kind ? ' ' + kind : '');
6181
+ }
6182
+
6183
+ function formatContextTokens(n) {
6184
+ return n >= 1000 ? Math.round(n / 1000) + 'k' : String(n);
6185
+ }
6186
+
6187
+ function makeEditField(labelText, input) {
6188
+ var field = document.createElement('div');
6189
+ field.className = 'cumulus-settings-edit-field';
6190
+ var caption = document.createElement('span');
6191
+ caption.textContent = labelText;
6192
+ field.appendChild(caption);
6193
+ field.appendChild(input);
6194
+ return field;
6195
+ }
6196
+
6197
+ // Task 130: rows are read-only text at rest (label + mono id + ctx chip +
6198
+ // default badge); ✎ flips one row into labeled inputs. `model.editing` is
6199
+ // UI-only state — serialization rebuilds entries from `model.source` +
6200
+ // explicit fields, so it never reaches the PUT payload.
6201
+ function renderModelRow(model, providerDef, index) {
6202
+ var row = document.createElement('div');
6203
+ row.className =
6204
+ 'cumulus-settings-model-row ' +
6205
+ model.provider +
6206
+ (model.gatewayDefault ? ' is-default' : '') +
6207
+ (model.editing ? ' editing' : '');
6208
+ row.setAttribute('data-gw-model', model.id);
6209
+ row.setAttribute('data-testid', 'webchat-gw-row-' + model.provider + '-' + index);
6210
+
6211
+ if (model.editing) {
6212
+ var grid = document.createElement('div');
6213
+ grid.className = 'cumulus-settings-edit-grid';
6214
+
6215
+ var idInput = document.createElement('input');
6216
+ idInput.type = 'text';
6217
+ idInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6218
+ idInput.value = model.id;
6219
+ idInput.setAttribute('aria-label', providerDef.title + ' model ID');
6220
+ idInput.setAttribute('data-testid', 'webchat-gw-id-' + model.provider + '-' + index);
6221
+ idInput.addEventListener('input', function () {
6222
+ model.id = idInput.value;
6223
+ });
6224
+ grid.appendChild(makeEditField('Model ID', idInput));
6225
+
6226
+ var labelInput = document.createElement('input');
6227
+ labelInput.type = 'text';
6228
+ labelInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6229
+ labelInput.value = model.label || model.id;
6230
+ labelInput.setAttribute('aria-label', providerDef.title + ' display label');
6231
+ labelInput.setAttribute(
6232
+ 'data-testid',
6233
+ 'webchat-gw-label-' + model.provider + '-' + index
6234
+ );
6235
+ labelInput.addEventListener('input', function () {
6236
+ model.label = labelInput.value;
6237
+ });
6238
+ grid.appendChild(makeEditField('Display label', labelInput));
6239
+
6240
+ if (model.provider !== 'anthropic') {
6241
+ var contextInput = document.createElement('input');
6242
+ contextInput.type = 'number';
6243
+ contextInput.min = '1';
6244
+ contextInput.step = '1';
6245
+ contextInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6246
+ contextInput.placeholder = '(default)';
6247
+ contextInput.value = model.contextWindow || '';
6248
+ contextInput.setAttribute('aria-label', providerDef.title + ' context window');
6249
+ contextInput.setAttribute(
6250
+ 'data-testid',
6251
+ 'webchat-gw-context-' + model.provider + '-' + index
6252
+ );
6253
+ contextInput.addEventListener('input', function () {
6254
+ model.contextWindow = contextInput.value ? Number(contextInput.value) : undefined;
6255
+ });
6256
+ grid.appendChild(makeEditField('Context tokens', contextInput));
6257
+ }
6258
+
6259
+ var doneBtn = document.createElement('button');
6260
+ doneBtn.type = 'button';
6261
+ doneBtn.textContent = 'Done';
6262
+ doneBtn.className = 'cumulus-settings-add-btn';
6263
+ doneBtn.setAttribute('data-testid', 'webchat-gw-done-' + model.provider + '-' + index);
6264
+ doneBtn.addEventListener('click', function () {
6265
+ model.editing = false;
6266
+ renderAll();
6267
+ });
6268
+ grid.appendChild(doneBtn);
6269
+ row.appendChild(grid);
6270
+ return row;
6271
+ }
6272
+
6273
+ var name = document.createElement('div');
6274
+ name.className = 'cumulus-settings-model-name';
6275
+ var labelEl = document.createElement('div');
6276
+ labelEl.className = 'cumulus-settings-model-label';
6277
+ labelEl.textContent = model.label || model.id;
6278
+ var idEl = document.createElement('div');
6279
+ idEl.className = 'cumulus-settings-model-id';
6280
+ idEl.textContent = model.id;
6281
+ name.appendChild(labelEl);
6282
+ name.appendChild(idEl);
6283
+ row.appendChild(name);
6284
+
6285
+ if (model.provider !== 'anthropic' && model.contextWindow) {
6286
+ var chip = document.createElement('span');
6287
+ chip.className = 'cumulus-settings-context-chip';
6288
+ chip.textContent = 'ctx ' + formatContextTokens(model.contextWindow);
6289
+ row.appendChild(chip);
6290
+ }
6291
+
6292
+ var badge = document.createElement('span');
6293
+ badge.className = 'cumulus-settings-default-badge';
6294
+ badge.textContent = 'default';
6295
+ row.appendChild(badge);
6296
+
6297
+ var actions = document.createElement('div');
6298
+ actions.className = 'cumulus-settings-row-actions';
6299
+
6300
+ var edit = document.createElement('button');
6301
+ edit.type = 'button';
6302
+ edit.className = 'cumulus-settings-row-edit';
6303
+ edit.textContent = '✎';
6304
+ edit.title = 'Edit ' + model.id;
6305
+ edit.setAttribute('data-testid', 'webchat-gw-edit-' + model.provider + '-' + index);
6306
+ edit.addEventListener('click', function () {
6307
+ model.editing = true;
6308
+ renderAll();
6309
+ });
6310
+ actions.appendChild(edit);
6311
+
6312
+ var remove = document.createElement('button');
6313
+ remove.type = 'button';
6314
+ remove.className = 'cumulus-settings-model-remove';
6315
+ remove.textContent = '\xD7';
6316
+ remove.title = 'Remove ' + model.id;
6317
+ remove.setAttribute('data-testid', 'webchat-gw-remove-' + model.provider + '-' + index);
6318
+ remove.addEventListener('click', function () {
6319
+ var absoluteIndex = gwState.catalog.indexOf(model);
6320
+ var wasGatewayDefault = model.gatewayDefault;
6321
+ var wasProviderDefault = model.providerDefault;
6322
+ if (absoluteIndex >= 0) gwState.catalog.splice(absoluteIndex, 1);
6323
+ if (wasGatewayDefault && gwState.catalog.length) {
6324
+ applyGatewayDefault(gwState.catalog, gwState.catalog[0].id);
6325
+ }
6326
+ if (
6327
+ wasProviderDefault &&
6328
+ !gwState.catalog.some(function (item) {
6329
+ return item.provider === 'anthropic' && item.providerDefault;
6330
+ })
6331
+ ) {
6332
+ var nextClaude = gwState.catalog.find(function (item) {
6333
+ return item.provider === 'anthropic';
6334
+ });
6335
+ if (nextClaude) nextClaude.providerDefault = true;
6336
+ }
6337
+ renderAll();
6338
+ });
6339
+ actions.appendChild(remove);
6340
+ row.appendChild(actions);
6341
+ return row;
6342
+ }
6343
+
6344
+ function renderCredential(card, provider) {
6345
+ if (!provider.credential) {
6346
+ var note = document.createElement('div');
6347
+ note.className = 'cumulus-settings-credential-note';
6348
+ note.textContent = 'Authenticate with `claude login` on the gateway host.';
6349
+ card.appendChild(note);
6350
+ return;
6351
+ }
6352
+ var credentialRow = document.createElement('div');
6353
+ credentialRow.className = 'cumulus-settings-provider-credential';
6354
+ var credentialLabel = document.createElement('label');
6355
+ credentialLabel.textContent = 'API key';
6356
+ var credentialInput = document.createElement('input');
6357
+ credentialInput.type = 'password';
6358
+ credentialInput.autocomplete = 'off';
6359
+ credentialInput.className = 'cumulus-settings-input';
6360
+ credentialInput.placeholder = credentialClears[provider.id]
6361
+ ? 'Will be cleared on save'
6362
+ : '(unset)';
6363
+ credentialInput.value = credentialValues[provider.id] || '';
6364
+ credentialInput.setAttribute('data-testid', provider.credentialTestId);
6365
+ credentialInput.addEventListener('input', function () {
6366
+ credentialValues[provider.id] = credentialInput.value;
6367
+ credentialClears[provider.id] = false;
6368
+ credentialInput.placeholder = '(unset)';
6369
+ });
6370
+ credentialInputs[provider.id] = credentialInput;
6371
+ var clear = document.createElement('button');
6372
+ clear.type = 'button';
6373
+ clear.className = 'cumulus-settings-clear-btn';
6374
+ clear.textContent = 'Clear';
6375
+ clear.setAttribute('data-testid', 'webchat-gw-clear-' + provider.id + '-key');
6376
+ clear.addEventListener('click', function () {
6377
+ credentialClears[provider.id] = true;
6378
+ credentialValues[provider.id] = '';
6379
+ credentialInput.value = '';
6380
+ credentialInput.placeholder = 'Will be cleared on save';
6381
+ setSettingsStatus('Credential marked for removal', '');
6382
+ });
6383
+ credentialRow.appendChild(credentialLabel);
6384
+ credentialRow.appendChild(credentialInput);
6385
+ credentialRow.appendChild(clear);
6386
+ card.appendChild(credentialRow);
6387
+ }
6388
+
6389
+ function renderGwModels() {
6390
+ gwList.innerHTML = '';
6391
+ credentialInputs = {};
6392
+ providerDefs.forEach(function (provider) {
6393
+ var providerModels = gwState.catalog.filter(function (model) {
6394
+ return model.provider === provider.id;
6395
+ });
6396
+ var card = document.createElement('section');
6397
+ card.className = 'cumulus-settings-provider-card';
6398
+ card.setAttribute('data-testid', 'webchat-gw-provider-' + provider.id);
6399
+ var head = document.createElement('div');
6400
+ head.className = 'cumulus-settings-provider-head';
6401
+ var heading = document.createElement('div');
6402
+ var title = document.createElement('div');
6403
+ title.className = 'cumulus-settings-provider-title';
6404
+ title.textContent = provider.title;
6405
+ var description = document.createElement('div');
6406
+ description.className = 'cumulus-settings-provider-desc';
6407
+ description.textContent = provider.description;
6408
+ heading.appendChild(title);
6409
+ heading.appendChild(description);
6410
+ var count = document.createElement('span');
6411
+ count.className = 'cumulus-settings-provider-count';
6412
+ count.textContent =
6413
+ providerModels.length + (providerModels.length === 1 ? ' model' : ' models');
6414
+ head.appendChild(heading);
6415
+ head.appendChild(count);
6416
+ card.appendChild(head);
6417
+ var list = document.createElement('div');
6418
+ list.className = 'cumulus-settings-provider-models';
6419
+ if (!providerModels.length) {
6420
+ var empty = document.createElement('div');
6421
+ empty.className = 'cumulus-settings-provider-empty';
6422
+ empty.textContent = 'No ' + provider.title + ' models configured.';
6423
+ list.appendChild(empty);
6424
+ } else {
6425
+ providerModels.forEach(function (model, index) {
6426
+ list.appendChild(renderModelRow(model, provider, index));
6427
+ });
6428
+ }
6429
+ card.appendChild(list);
6430
+ // The Claude sub-model default only surfaces when it can actually
6431
+ // diverge from the gateway default (task 130) — gateway default on a
6432
+ // direct provider, ≥2 Anthropic models. Otherwise coupling makes it
6433
+ // redundant and it stays out of the way.
6434
+ if (provider.id === 'anthropic' && claudeSubDefaultVisible(gwState.catalog)) {
6435
+ var subRow = document.createElement('div');
6436
+ subRow.className = 'cumulus-settings-claude-default-row';
6437
+ var subLabel = document.createElement('label');
6438
+ subLabel.textContent = 'When a thread picks Claude, use';
6439
+ var subSelect = document.createElement('select');
6440
+ subSelect.className = 'cumulus-settings-select';
6441
+ subSelect.setAttribute('data-testid', 'webchat-gw-claude-default-select');
6442
+ var subDefault = '';
6443
+ providerModels.forEach(function (model) {
6444
+ var opt = document.createElement('option');
6445
+ opt.value = model.id;
6446
+ opt.textContent = model.label || model.id;
6447
+ subSelect.appendChild(opt);
6448
+ if (model.providerDefault) subDefault = model.id;
6449
+ });
6450
+ subSelect.value = subDefault || (providerModels[0] && providerModels[0].id) || '';
6451
+ subSelect.addEventListener('change', function () {
6452
+ applyClaudeSubDefault(gwState.catalog, subSelect.value);
6453
+ });
6454
+ subRow.appendChild(subLabel);
6455
+ subRow.appendChild(subSelect);
6456
+ card.appendChild(subRow);
6457
+ }
6458
+ renderCredential(card, provider);
6459
+ gwList.appendChild(card);
6460
+ });
6461
+ }
6462
+
6463
+ function renderDefaultSelect() {
6464
+ defaultSelect.innerHTML = '';
6465
+ if (!gwState.catalog.length) {
6466
+ var none = document.createElement('option');
6467
+ none.value = '';
6468
+ none.textContent = 'No models configured';
6469
+ defaultSelect.appendChild(none);
6470
+ defaultSelect.disabled = true;
6471
+ return;
6472
+ }
6473
+ defaultSelect.disabled = false;
6474
+ providerDefs.forEach(function (provider) {
6475
+ var group = null;
6476
+ gwState.catalog.forEach(function (model) {
6477
+ if (model.provider !== provider.id) return;
6478
+ if (!group) {
6479
+ group = document.createElement('optgroup');
6480
+ group.label = provider.title;
6481
+ defaultSelect.appendChild(group);
6482
+ }
6483
+ var opt = document.createElement('option');
6484
+ opt.value = model.id;
6485
+ opt.textContent = model.label || model.id;
6486
+ group.appendChild(opt);
6487
+ });
6488
+ });
6489
+ var current = gwState.catalog.find(function (model) {
6490
+ return model.gatewayDefault;
6491
+ });
6492
+ defaultSelect.value = current ? current.id : gwState.catalog[0].id;
6493
+ }
6494
+
6495
+ function renderAll() {
6496
+ renderDefaultSelect();
6497
+ renderGwModels();
6498
+ }
6499
+
6500
+ addProvider.addEventListener('change', function () {
6501
+ addContext.disabled = addProvider.value === 'anthropic';
6502
+ if (addContext.disabled) addContext.value = '';
6503
+ });
6504
+ addBtn.addEventListener('click', function () {
6505
+ var id = (addId.value || '').trim();
6506
+ var provider = addProvider.value;
6507
+ var context = addContext.value ? Number(addContext.value) : undefined;
6508
+ if (!id) {
6509
+ setSettingsStatus('Model ID is required', 'error');
6510
+ return;
6511
+ }
6512
+ if (
6513
+ id === 'claude' ||
6514
+ gwState.catalog.some(function (item) {
6515
+ return item.id === id;
6516
+ })
6517
+ ) {
6518
+ setSettingsStatus('Duplicate or reserved model ID: ' + id, 'error');
6519
+ return;
6520
+ }
6521
+ if (context !== undefined && (!Number.isInteger(context) || context <= 0)) {
6522
+ setSettingsStatus('Context window must be a positive whole number', 'error');
6523
+ return;
6524
+ }
6525
+ var model = {
6526
+ id: id,
6527
+ label: (addLabel.value || '').trim() || id,
6528
+ provider: provider,
6529
+ contextWindow: provider === 'anthropic' ? undefined : context,
6530
+ gatewayDefault: gwState.catalog.length === 0,
6531
+ providerDefault:
6532
+ provider === 'anthropic' &&
6533
+ !gwState.catalog.some(function (item) {
6534
+ return item.provider === 'anthropic';
6535
+ }),
6536
+ };
6537
+ gwState.catalog.push(model);
6538
+ addId.value = '';
6539
+ addLabel.value = '';
6540
+ addContext.value = '';
6541
+ setSettingsStatus('', '');
6542
+ renderAll();
6543
+ });
6544
+
6545
+ function refreshOpenModelCatalogs() {
6546
+ var refresh = new XMLHttpRequest();
6547
+ refresh.open('GET', '/api/models');
6548
+ refresh.setRequestHeader('X-API-Key', activeApiKey);
6549
+ refresh.onreadystatechange = function () {
6550
+ if (refresh.readyState !== 4 || refresh.status !== 200) return;
6551
+ try {
6552
+ window.dispatchEvent(
6553
+ new CustomEvent('cumulus-model-catalog-updated', {
6554
+ detail: JSON.parse(refresh.responseText),
6555
+ })
6556
+ );
6557
+ } catch (e) {
6558
+ /* keep the saved config even if a panel refresh fails */
6559
+ }
6560
+ };
6561
+ refresh.send();
6562
+ }
6563
+
6564
+ saveBtn.addEventListener('click', function () {
6565
+ var validationError = validateModelCatalog(gwState.catalog);
6566
+ if (validationError) {
6567
+ setSettingsStatus(validationError, 'error');
6568
+ return;
6569
+ }
6570
+ saveBtn.disabled = true;
6571
+ setSettingsStatus('Saving…', '');
6572
+ var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), {
6573
+ openai: {
6574
+ value: credentialValues.openai,
6575
+ clear: credentialClears.openai,
6576
+ },
6577
+ huggingface: {
6578
+ value: credentialValues.huggingface,
6579
+ clear: credentialClears.huggingface,
6580
+ },
6581
+ license: {
6582
+ value: credentialValues.license,
6583
+ clear: credentialClears.license,
6584
+ },
6585
+ });
6586
+ var save = new XMLHttpRequest();
6587
+ save.open('PUT', '/api/config');
6588
+ save.setRequestHeader('Content-Type', 'application/json');
6589
+ save.setRequestHeader('X-API-Key', activeApiKey);
6590
+ save.onreadystatechange = function () {
6591
+ if (save.readyState !== 4) return;
6592
+ saveBtn.disabled = false;
6593
+ if (save.status === 200) {
6594
+ try {
6595
+ var response = JSON.parse(save.responseText);
6596
+ gwState = gatewayConfigToModelCatalog(response);
6597
+ credentialClears.openai = false;
6598
+ credentialClears.huggingface = false;
6599
+ credentialClears.license = false;
6600
+ credentialValues.openai = response.openaiApiKey || '';
6601
+ credentialValues.huggingface = response.hfApiKey || '';
6602
+ credentialValues.license = response.licenseKey || '';
6603
+ gwLicenseMasked = response.licenseKey || '';
6604
+ gwLicenseInfo = response.license || null;
6605
+ renderAll();
6606
+ renderLicenseState(gwLicenseMasked, gwLicenseInfo);
6607
+ setSettingsStatus(
6608
+ gwLicenseInfo && gwLicenseInfo.pendingReload
6609
+ ? 'Saved — license takes effect on gateway reload'
6610
+ : 'Saved — applies to new turns immediately',
6611
+ 'success'
6612
+ );
6613
+ refreshOpenModelCatalogs();
6614
+ } catch (e) {
6615
+ setSettingsStatus('Saved, but the response could not be rendered', 'error');
6616
+ }
6617
+ } else {
6618
+ setSettingsStatus('Error ' + save.status, 'error');
6619
+ }
6620
+ };
6621
+ save.send(JSON.stringify(payload));
6622
+ });
6623
+
6624
+ (function loadGatewayConfig() {
6625
+ var load = new XMLHttpRequest();
6626
+ load.open('GET', '/api/config');
6627
+ load.setRequestHeader('X-API-Key', activeApiKey);
6628
+ load.onreadystatechange = function () {
6629
+ if (load.readyState !== 4) return;
6630
+ if (load.status === 200) {
6631
+ try {
6632
+ var response = JSON.parse(load.responseText);
6633
+ gwState = gatewayConfigToModelCatalog(response);
6634
+ credentialValues.openai = response.openaiApiKey || '';
6635
+ credentialValues.huggingface = response.hfApiKey || '';
6636
+ credentialValues.license = response.licenseKey || '';
6637
+ gwLicenseMasked = response.licenseKey || '';
6638
+ gwLicenseInfo = response.license || null;
6639
+ renderAll();
6640
+ renderLicenseState(gwLicenseMasked, gwLicenseInfo);
6641
+ } catch (e) {
6642
+ setSettingsStatus('Gateway settings response was invalid', 'error');
6643
+ }
6644
+ } else {
6645
+ setSettingsStatus('Load failed (' + load.status + ')', 'error');
6646
+ }
6647
+ };
6648
+ load.send();
6649
+ })();
6650
+ }
6651
+
5849
6652
  function buildThreadPanel(threadName) {
5850
6653
  var state = getThreadState(threadName);
5851
6654
 
@@ -5907,14 +6710,14 @@
5907
6710
  claudeModelSelect.setAttribute('data-testid', 'webchat-claude-model-select');
5908
6711
  claudeModelSelect.setAttribute('data-thread-claude-model', threadName);
5909
6712
  claudeModelSelect.setAttribute('title', 'Claude model');
5910
- // First-paint / old-server fallback onlythe authoritative list comes from
5911
- // /api/models (resp.claudeModels), populated below in loadModels().
5912
- var CLAUDE_MODELS_FALLBACK = [
5913
- { id: 'claude-opus-5[1m]', label: 'Opus 5', default: true },
5914
- { id: 'claude-sonnet-5[1m]', label: 'Sonnet 5' },
5915
- ];
6713
+ // Populated ONLY from /api/models (resp.claudeModels)task 129 deleted
6714
+ // the hardcoded fallback list. An empty catalog hides the select entirely
6715
+ // (visibility is re-derived in updateEffortVisibility, which also gates
6716
+ // on model === 'claude'); the Settings gear is where models get added.
6717
+ var claudeModelListEmpty = true;
5916
6718
  function populateClaudeModels(list) {
5917
- var models = list && list.length ? list : CLAUDE_MODELS_FALLBACK;
6719
+ var models = list && list.length ? list : [];
6720
+ claudeModelListEmpty = models.length === 0;
5918
6721
  claudeModelSelect.innerHTML = '';
5919
6722
  var def = null;
5920
6723
  models.forEach(function (m) {
@@ -5925,6 +6728,7 @@
5925
6728
  if (m.default) def = m.id;
5926
6729
  });
5927
6730
  claudeModelSelect.value = def || (models[0] && models[0].id) || '';
6731
+ claudeModelSelect.style.display = claudeModelListEmpty ? 'none' : '';
5928
6732
  }
5929
6733
  populateClaudeModels(null);
5930
6734
 
@@ -6024,7 +6828,7 @@
6024
6828
  function updateEffortVisibility() {
6025
6829
  var isClaude = modelSelect.value === 'claude';
6026
6830
  effortSelect.style.display = isClaude ? '' : 'none';
6027
- claudeModelSelect.style.display = isClaude ? '' : 'none';
6831
+ claudeModelSelect.style.display = isClaude && !claudeModelListEmpty ? '' : 'none';
6028
6832
  // Hide the whole Model/Effort popover rows (label included) for non-Claude providers.
6029
6833
  var mr = panel.querySelector('[data-config-row="model"]');
6030
6834
  var er = panel.querySelector('[data-config-row="effort"]');
@@ -6185,546 +6989,13 @@
6185
6989
  modelPicker.appendChild(configBreadcrumb);
6186
6990
  modelPicker.appendChild(modelPopover);
6187
6991
 
6188
- // ── Settings modal (provider-organized model catalog + credentials) ──
6189
- // Live-editable via PUT /api/config (task 104) — takes effect on the NEXT turn
6190
- // with no restart, no JSON edit, no republish. Any valid gateway key.
6191
- function openSettingsModal() {
6192
- var backdrop = document.createElement('div');
6193
- backdrop.className = 'cumulus-settings-backdrop';
6194
- backdrop.setAttribute('data-testid', 'webchat-settings-modal');
6195
- var modal = document.createElement('div');
6196
- modal.className = 'cumulus-settings-modal';
6197
-
6198
- var mHeader = document.createElement('div');
6199
- mHeader.className = 'cumulus-settings-header';
6200
- var mHeaderText = document.createElement('div');
6201
- var mTitle = document.createElement('div');
6202
- mTitle.className = 'cumulus-settings-title';
6203
- mTitle.textContent = 'Gateway settings';
6204
- var mSub = document.createElement('div');
6205
- mSub.className = 'cumulus-settings-subtitle';
6206
- mSub.textContent =
6207
- 'Manage Anthropic, OpenAI, and HuggingFace models — changes apply to new turns instantly';
6208
- mHeaderText.appendChild(mTitle);
6209
- mHeaderText.appendChild(mSub);
6210
- var mClose = document.createElement('button');
6211
- mClose.className = 'cumulus-overlay-close';
6212
- mClose.textContent = '\xD7';
6213
- mClose.setAttribute('data-testid', 'webchat-settings-close');
6214
- mClose.addEventListener('click', function () {
6215
- backdrop.remove();
6216
- });
6217
- mHeader.appendChild(mHeaderText);
6218
- mHeader.appendChild(mClose);
6219
- modal.appendChild(mHeader);
6220
-
6221
- var body = document.createElement('div');
6222
- body.className = 'cumulus-settings-body';
6223
-
6224
- var modelsLabel = document.createElement('div');
6225
- modelsLabel.className = 'cumulus-settings-section-label';
6226
- modelsLabel.textContent = 'Models by provider';
6227
- body.appendChild(modelsLabel);
6228
-
6229
- var gwList = document.createElement('div');
6230
- gwList.setAttribute('data-testid', 'webchat-gateway-models');
6231
- body.appendChild(gwList);
6232
-
6233
- var addCard = document.createElement('div');
6234
- addCard.className = 'cumulus-settings-add-card';
6235
- var addTitle = document.createElement('div');
6236
- addTitle.className = 'cumulus-settings-add-title';
6237
- addTitle.textContent = 'Add a model';
6238
- addCard.appendChild(addTitle);
6239
- var addWrap = document.createElement('div');
6240
- addWrap.className = 'cumulus-settings-add-row';
6241
- var addProvider = document.createElement('select');
6242
- addProvider.className = 'cumulus-settings-select';
6243
- addProvider.setAttribute('data-testid', 'webchat-gw-add-provider');
6244
- [
6245
- { value: 'anthropic', label: 'Anthropic' },
6246
- { value: 'openai', label: 'OpenAI' },
6247
- { value: 'huggingface', label: 'HuggingFace' },
6248
- ].forEach(function (provider) {
6249
- var option = document.createElement('option');
6250
- option.value = provider.value;
6251
- option.textContent = provider.label;
6252
- addProvider.appendChild(option);
6253
- });
6254
- var addId = document.createElement('input');
6255
- addId.type = 'text';
6256
- addId.className = 'cumulus-settings-input';
6257
- addId.placeholder = 'Model ID / name';
6258
- addId.setAttribute('data-testid', 'webchat-gw-add-id');
6259
- var addLabel = document.createElement('input');
6260
- addLabel.type = 'text';
6261
- addLabel.className = 'cumulus-settings-input';
6262
- addLabel.placeholder = 'Display label (optional)';
6263
- addLabel.setAttribute('data-testid', 'webchat-gw-add-label');
6264
- var addContext = document.createElement('input');
6265
- addContext.type = 'number';
6266
- addContext.min = '1';
6267
- addContext.step = '1';
6268
- addContext.className = 'cumulus-settings-input';
6269
- addContext.placeholder = 'Context tokens';
6270
- addContext.setAttribute('data-testid', 'webchat-gw-add-context');
6271
- var addBtn = document.createElement('button');
6272
- addBtn.type = 'button';
6273
- addBtn.textContent = '+ Add';
6274
- addBtn.className = 'cumulus-settings-add-btn';
6275
- addBtn.setAttribute('data-testid', 'webchat-gw-add-btn');
6276
- addWrap.appendChild(addProvider);
6277
- addWrap.appendChild(addId);
6278
- addWrap.appendChild(addLabel);
6279
- addWrap.appendChild(addContext);
6280
- addWrap.appendChild(addBtn);
6281
- addCard.appendChild(addWrap);
6282
- body.appendChild(addCard);
6283
- modal.appendChild(body);
6284
-
6285
- var footer = document.createElement('div');
6286
- footer.className = 'cumulus-settings-footer';
6287
- var saveBtn = document.createElement('button');
6288
- saveBtn.textContent = 'Save';
6289
- saveBtn.className = 'cumulus-settings-btn';
6290
- saveBtn.setAttribute('data-testid', 'webchat-gw-save');
6291
- var gwStatus = document.createElement('span');
6292
- gwStatus.className = 'cumulus-settings-status';
6293
- footer.appendChild(saveBtn);
6294
- footer.appendChild(gwStatus);
6295
- modal.appendChild(footer);
6296
-
6297
- backdrop.appendChild(modal);
6298
- backdrop.addEventListener('click', function (e) {
6299
- if (e.target === backdrop) backdrop.remove();
6300
- });
6301
- document.body.appendChild(backdrop);
6302
-
6303
- var gwState = { catalog: [], sentinel: null };
6304
- var credentialInputs = {};
6305
- var credentialValues = { openai: '', huggingface: '' };
6306
- var credentialClears = { openai: false, huggingface: false };
6307
- var providerDefs = [
6308
- {
6309
- id: 'anthropic',
6310
- title: 'Anthropic',
6311
- description: 'Claude CLI models · authentication is managed by the Claude CLI',
6312
- },
6313
- {
6314
- id: 'openai',
6315
- title: 'OpenAI',
6316
- description: 'Responses API models with direct tool use',
6317
- credential: 'openaiApiKey',
6318
- credentialTestId: 'webchat-gw-openaikey',
6319
- },
6320
- {
6321
- id: 'huggingface',
6322
- title: 'HuggingFace',
6323
- description: 'OpenAI-compatible models served through the HuggingFace router',
6324
- credential: 'hfApiKey',
6325
- credentialTestId: 'webchat-gw-hfkey',
6326
- },
6327
- ];
6328
-
6329
- function setSettingsStatus(message, kind) {
6330
- gwStatus.textContent = message || '';
6331
- gwStatus.className = 'cumulus-settings-status' + (kind ? ' ' + kind : '');
6332
- }
6333
-
6334
- function makeRadioLabel(text, input) {
6335
- var wrap = document.createElement('label');
6336
- wrap.className = 'cumulus-settings-radio-label';
6337
- wrap.appendChild(input);
6338
- wrap.appendChild(document.createElement('br'));
6339
- wrap.appendChild(document.createTextNode(text));
6340
- return wrap;
6341
- }
6342
-
6343
- function renderModelRow(model, providerModels, index) {
6344
- var row = document.createElement('div');
6345
- row.className =
6346
- 'cumulus-settings-model-row ' +
6347
- model.provider +
6348
- (model.gatewayDefault ? ' is-default' : '');
6349
- row.setAttribute('data-gw-model', model.id);
6350
- row.setAttribute('data-testid', 'webchat-gw-row-' + model.provider + '-' + index);
6351
-
6352
- var gatewayRadio = document.createElement('input');
6353
- gatewayRadio.type = 'radio';
6354
- gatewayRadio.name = 'gw-default';
6355
- gatewayRadio.checked = !!model.gatewayDefault;
6356
- gatewayRadio.setAttribute(
6357
- 'data-testid',
6358
- 'webchat-gw-default-' + model.provider + '-' + index
6359
- );
6360
- gatewayRadio.addEventListener('change', function () {
6361
- gwState.catalog.forEach(function (item) {
6362
- item.gatewayDefault = false;
6363
- });
6364
- model.gatewayDefault = true;
6365
- if (model.provider === 'anthropic') {
6366
- gwState.catalog.forEach(function (item) {
6367
- if (item.provider === 'anthropic') item.providerDefault = false;
6368
- });
6369
- model.providerDefault = true;
6370
- }
6371
- renderGwModels();
6372
- });
6373
- row.appendChild(makeRadioLabel('gateway', gatewayRadio));
6374
-
6375
- if (model.provider === 'anthropic') {
6376
- var providerRadio = document.createElement('input');
6377
- providerRadio.type = 'radio';
6378
- providerRadio.name = 'gw-anthropic-default';
6379
- providerRadio.checked = !!model.providerDefault;
6380
- providerRadio.setAttribute(
6381
- 'data-testid',
6382
- 'webchat-gw-provider-default-anthropic-' + index
6383
- );
6384
- providerRadio.addEventListener('change', function () {
6385
- gwState.catalog.forEach(function (item) {
6386
- if (item.provider === 'anthropic') item.providerDefault = false;
6387
- });
6388
- model.providerDefault = true;
6389
- if (
6390
- gwState.catalog.some(function (item) {
6391
- return item.provider === 'anthropic' && item.gatewayDefault;
6392
- })
6393
- ) {
6394
- gwState.catalog.forEach(function (item) {
6395
- item.gatewayDefault = false;
6396
- });
6397
- model.gatewayDefault = true;
6398
- }
6399
- renderGwModels();
6400
- });
6401
- row.appendChild(makeRadioLabel('Claude', providerRadio));
6402
- } else {
6403
- var spacer = document.createElement('span');
6404
- row.appendChild(spacer);
6405
- }
6406
-
6407
- var idInput = document.createElement('input');
6408
- idInput.type = 'text';
6409
- idInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6410
- idInput.value = model.id;
6411
- idInput.setAttribute('aria-label', providerModels.title + ' model ID');
6412
- idInput.setAttribute('data-testid', 'webchat-gw-id-' + model.provider + '-' + index);
6413
- idInput.addEventListener('input', function () {
6414
- model.id = idInput.value;
6415
- });
6416
- row.appendChild(idInput);
6417
-
6418
- var labelInput = document.createElement('input');
6419
- labelInput.type = 'text';
6420
- labelInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6421
- labelInput.value = model.label || model.id;
6422
- labelInput.setAttribute('aria-label', providerModels.title + ' display label');
6423
- labelInput.setAttribute(
6424
- 'data-testid',
6425
- 'webchat-gw-label-' + model.provider + '-' + index
6426
- );
6427
- labelInput.addEventListener('input', function () {
6428
- model.label = labelInput.value;
6429
- });
6430
- row.appendChild(labelInput);
6431
-
6432
- if (model.provider !== 'anthropic') {
6433
- var contextInput = document.createElement('input');
6434
- contextInput.type = 'number';
6435
- contextInput.min = '1';
6436
- contextInput.step = '1';
6437
- contextInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6438
- contextInput.placeholder = 'Context';
6439
- contextInput.value = model.contextWindow || '';
6440
- contextInput.setAttribute('aria-label', providerModels.title + ' context window');
6441
- contextInput.setAttribute(
6442
- 'data-testid',
6443
- 'webchat-gw-context-' + model.provider + '-' + index
6444
- );
6445
- contextInput.addEventListener('input', function () {
6446
- model.contextWindow = contextInput.value ? Number(contextInput.value) : undefined;
6447
- });
6448
- row.appendChild(contextInput);
6449
- }
6450
-
6451
- var remove = document.createElement('button');
6452
- remove.type = 'button';
6453
- remove.className = 'cumulus-settings-model-remove';
6454
- remove.textContent = '\xD7';
6455
- remove.title = 'Remove ' + model.id;
6456
- remove.setAttribute('data-testid', 'webchat-gw-remove-' + model.provider + '-' + index);
6457
- remove.addEventListener('click', function () {
6458
- var absoluteIndex = gwState.catalog.indexOf(model);
6459
- var wasGatewayDefault = model.gatewayDefault;
6460
- var wasProviderDefault = model.providerDefault;
6461
- if (absoluteIndex >= 0) gwState.catalog.splice(absoluteIndex, 1);
6462
- if (wasGatewayDefault && gwState.catalog.length) {
6463
- var nextGatewayDefault = gwState.catalog[0];
6464
- nextGatewayDefault.gatewayDefault = true;
6465
- if (nextGatewayDefault.provider === 'anthropic') {
6466
- gwState.catalog.forEach(function (item) {
6467
- if (item.provider === 'anthropic') item.providerDefault = false;
6468
- });
6469
- nextGatewayDefault.providerDefault = true;
6470
- }
6471
- }
6472
- if (
6473
- wasProviderDefault &&
6474
- !gwState.catalog.some(function (item) {
6475
- return item.provider === 'anthropic' && item.providerDefault;
6476
- })
6477
- ) {
6478
- var nextClaude = gwState.catalog.find(function (item) {
6479
- return item.provider === 'anthropic';
6480
- });
6481
- if (nextClaude) nextClaude.providerDefault = true;
6482
- }
6483
- renderGwModels();
6484
- });
6485
- row.appendChild(remove);
6486
- return row;
6487
- }
6488
-
6489
- function renderCredential(card, provider) {
6490
- if (!provider.credential) {
6491
- var note = document.createElement('div');
6492
- note.className = 'cumulus-settings-credential-note';
6493
- note.textContent = 'Authenticate with `claude login` on the gateway host.';
6494
- card.appendChild(note);
6495
- return;
6496
- }
6497
- var credentialRow = document.createElement('div');
6498
- credentialRow.className = 'cumulus-settings-provider-credential';
6499
- var credentialLabel = document.createElement('label');
6500
- credentialLabel.textContent = 'API key';
6501
- var credentialInput = document.createElement('input');
6502
- credentialInput.type = 'password';
6503
- credentialInput.autocomplete = 'off';
6504
- credentialInput.className = 'cumulus-settings-input';
6505
- credentialInput.placeholder = credentialClears[provider.id]
6506
- ? 'Will be cleared on save'
6507
- : '(unset)';
6508
- credentialInput.value = credentialValues[provider.id] || '';
6509
- credentialInput.setAttribute('data-testid', provider.credentialTestId);
6510
- credentialInput.addEventListener('input', function () {
6511
- credentialValues[provider.id] = credentialInput.value;
6512
- credentialClears[provider.id] = false;
6513
- credentialInput.placeholder = '(unset)';
6514
- });
6515
- credentialInputs[provider.id] = credentialInput;
6516
- var clear = document.createElement('button');
6517
- clear.type = 'button';
6518
- clear.className = 'cumulus-settings-clear-btn';
6519
- clear.textContent = 'Clear';
6520
- clear.setAttribute('data-testid', 'webchat-gw-clear-' + provider.id + '-key');
6521
- clear.addEventListener('click', function () {
6522
- credentialClears[provider.id] = true;
6523
- credentialValues[provider.id] = '';
6524
- credentialInput.value = '';
6525
- credentialInput.placeholder = 'Will be cleared on save';
6526
- setSettingsStatus('Credential marked for removal', '');
6527
- });
6528
- credentialRow.appendChild(credentialLabel);
6529
- credentialRow.appendChild(credentialInput);
6530
- credentialRow.appendChild(clear);
6531
- card.appendChild(credentialRow);
6532
- }
6533
-
6534
- function renderGwModels() {
6535
- gwList.innerHTML = '';
6536
- credentialInputs = {};
6537
- providerDefs.forEach(function (provider) {
6538
- var providerModels = gwState.catalog.filter(function (model) {
6539
- return model.provider === provider.id;
6540
- });
6541
- var card = document.createElement('section');
6542
- card.className = 'cumulus-settings-provider-card';
6543
- card.setAttribute('data-testid', 'webchat-gw-provider-' + provider.id);
6544
- var head = document.createElement('div');
6545
- head.className = 'cumulus-settings-provider-head';
6546
- var heading = document.createElement('div');
6547
- var title = document.createElement('div');
6548
- title.className = 'cumulus-settings-provider-title';
6549
- title.textContent = provider.title;
6550
- var description = document.createElement('div');
6551
- description.className = 'cumulus-settings-provider-desc';
6552
- description.textContent = provider.description;
6553
- heading.appendChild(title);
6554
- heading.appendChild(description);
6555
- var count = document.createElement('span');
6556
- count.className = 'cumulus-settings-provider-count';
6557
- count.textContent =
6558
- providerModels.length + (providerModels.length === 1 ? ' model' : ' models');
6559
- head.appendChild(heading);
6560
- head.appendChild(count);
6561
- card.appendChild(head);
6562
- var list = document.createElement('div');
6563
- list.className = 'cumulus-settings-provider-models';
6564
- if (!providerModels.length) {
6565
- var empty = document.createElement('div');
6566
- empty.className = 'cumulus-settings-provider-empty';
6567
- empty.textContent = 'No ' + provider.title + ' models configured.';
6568
- list.appendChild(empty);
6569
- } else {
6570
- providerModels.forEach(function (model, index) {
6571
- list.appendChild(renderModelRow(model, provider, index));
6572
- });
6573
- }
6574
- card.appendChild(list);
6575
- renderCredential(card, provider);
6576
- gwList.appendChild(card);
6577
- });
6578
- }
6579
-
6580
- addProvider.addEventListener('change', function () {
6581
- addContext.disabled = addProvider.value === 'anthropic';
6582
- if (addContext.disabled) addContext.value = '';
6583
- });
6584
- addBtn.addEventListener('click', function () {
6585
- var id = (addId.value || '').trim();
6586
- var provider = addProvider.value;
6587
- var context = addContext.value ? Number(addContext.value) : undefined;
6588
- if (!id) {
6589
- setSettingsStatus('Model ID is required', 'error');
6590
- return;
6591
- }
6592
- if (
6593
- id === 'claude' ||
6594
- gwState.catalog.some(function (item) {
6595
- return item.id === id;
6596
- })
6597
- ) {
6598
- setSettingsStatus('Duplicate or reserved model ID: ' + id, 'error');
6599
- return;
6600
- }
6601
- if (context !== undefined && (!Number.isInteger(context) || context <= 0)) {
6602
- setSettingsStatus('Context window must be a positive whole number', 'error');
6603
- return;
6604
- }
6605
- var model = {
6606
- id: id,
6607
- label: (addLabel.value || '').trim() || id,
6608
- provider: provider,
6609
- contextWindow: provider === 'anthropic' ? undefined : context,
6610
- gatewayDefault: gwState.catalog.length === 0,
6611
- providerDefault:
6612
- provider === 'anthropic' &&
6613
- !gwState.catalog.some(function (item) {
6614
- return item.provider === 'anthropic';
6615
- }),
6616
- };
6617
- gwState.catalog.push(model);
6618
- addId.value = '';
6619
- addLabel.value = '';
6620
- addContext.value = '';
6621
- setSettingsStatus('', '');
6622
- renderGwModels();
6623
- });
6624
-
6625
- function refreshOpenModelCatalogs() {
6626
- var refresh = new XMLHttpRequest();
6627
- refresh.open('GET', '/api/models');
6628
- refresh.setRequestHeader('X-API-Key', activeApiKey);
6629
- refresh.onreadystatechange = function () {
6630
- if (refresh.readyState !== 4 || refresh.status !== 200) return;
6631
- try {
6632
- window.dispatchEvent(
6633
- new CustomEvent('cumulus-model-catalog-updated', {
6634
- detail: JSON.parse(refresh.responseText),
6635
- })
6636
- );
6637
- } catch (e) {
6638
- /* keep the saved config even if a panel refresh fails */
6639
- }
6640
- };
6641
- refresh.send();
6642
- }
6643
-
6644
- saveBtn.addEventListener('click', function () {
6645
- var validationError = validateModelCatalog(gwState.catalog);
6646
- if (validationError) {
6647
- setSettingsStatus(validationError, 'error');
6648
- return;
6649
- }
6650
- saveBtn.disabled = true;
6651
- setSettingsStatus('Saving…', '');
6652
- var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), {
6653
- openai: {
6654
- value: credentialValues.openai,
6655
- clear: credentialClears.openai,
6656
- },
6657
- huggingface: {
6658
- value: credentialValues.huggingface,
6659
- clear: credentialClears.huggingface,
6660
- },
6661
- });
6662
- var save = new XMLHttpRequest();
6663
- save.open('PUT', '/api/config');
6664
- save.setRequestHeader('Content-Type', 'application/json');
6665
- save.setRequestHeader('X-API-Key', activeApiKey);
6666
- save.onreadystatechange = function () {
6667
- if (save.readyState !== 4) return;
6668
- saveBtn.disabled = false;
6669
- if (save.status === 200) {
6670
- try {
6671
- var response = JSON.parse(save.responseText);
6672
- gwState = gatewayConfigToModelCatalog(response);
6673
- credentialClears.openai = false;
6674
- credentialClears.huggingface = false;
6675
- credentialValues.openai = response.openaiApiKey || '';
6676
- credentialValues.huggingface = response.hfApiKey || '';
6677
- renderGwModels();
6678
- setSettingsStatus('Saved — applies to new turns immediately', 'success');
6679
- refreshOpenModelCatalogs();
6680
- } catch (e) {
6681
- setSettingsStatus('Saved, but the response could not be rendered', 'error');
6682
- }
6683
- } else {
6684
- setSettingsStatus('Error ' + save.status, 'error');
6685
- }
6686
- };
6687
- save.send(JSON.stringify(payload));
6688
- });
6689
-
6690
- (function loadGatewayConfig() {
6691
- var load = new XMLHttpRequest();
6692
- load.open('GET', '/api/config');
6693
- load.setRequestHeader('X-API-Key', activeApiKey);
6694
- load.onreadystatechange = function () {
6695
- if (load.readyState !== 4) return;
6696
- if (load.status === 200) {
6697
- try {
6698
- var response = JSON.parse(load.responseText);
6699
- gwState = gatewayConfigToModelCatalog(response);
6700
- credentialValues.openai = response.openaiApiKey || '';
6701
- credentialValues.huggingface = response.hfApiKey || '';
6702
- renderGwModels();
6703
- } catch (e) {
6704
- setSettingsStatus('Gateway settings response was invalid', 'error');
6705
- }
6706
- } else {
6707
- setSettingsStatus('Load failed (' + load.status + ')', 'error');
6708
- }
6709
- };
6710
- load.send();
6711
- })();
6712
- }
6713
-
6714
6992
  panelHeader.appendChild(modelPicker);
6715
6993
 
6716
- // Action buttons (settings + include + revert)
6994
+ // Action buttons (include + revert + select) — gateway settings moved
6995
+ // to the topbar gear (task 128)
6717
6996
  var actions = document.createElement('div');
6718
6997
  actions.className = 'cumulus-panel-actions';
6719
6998
 
6720
- var settingsBtn = document.createElement('button');
6721
- settingsBtn.className = 'cumulus-panel-action-btn';
6722
- settingsBtn.setAttribute('data-testid', 'webchat-settings-btn');
6723
- settingsBtn.setAttribute('title', 'Settings — configure available models');
6724
- settingsBtn.textContent = '⚙'; // gear
6725
- settingsBtn.addEventListener('click', openSettingsModal);
6726
- actions.appendChild(settingsBtn);
6727
-
6728
6999
  var includeBtn = document.createElement('button');
6729
7000
  includeBtn.className = 'cumulus-panel-action-btn';
6730
7001
  includeBtn.setAttribute('data-testid', 'webchat-include-btn');
@@ -7175,7 +7446,7 @@
7175
7446
  el.className = 'cumulus-msg assistant';
7176
7447
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
7177
7448
  if (content) {
7178
- var mdResult = renderMarkdown(content);
7449
+ var mdResult = renderMarkdown(content, { streaming: isStreaming });
7179
7450
  el.innerHTML = mdResult.html;
7180
7451
  if (isStreaming) {
7181
7452
  el.innerHTML += '<span class="cumulus-cursor"></span>';