@luckydraw/cumulus 0.31.66 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +6 -559
  2. package/LICENSE +150 -0
  3. package/README.md +27 -8
  4. package/dist/gateway/adapters/webchat.d.ts +2 -0
  5. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  6. package/dist/gateway/adapters/webchat.js +22 -2
  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/widget.js +897 -611
  29. package/dist/lib/gateway.d.ts +30 -8
  30. package/dist/lib/gateway.d.ts.map +1 -1
  31. package/dist/lib/gateway.js +36 -11
  32. package/dist/lib/gateway.js.map +1 -1
  33. package/dist/lib/history.d.ts +22 -0
  34. package/dist/lib/history.d.ts.map +1 -1
  35. package/dist/lib/history.js +59 -21
  36. package/dist/lib/history.js.map +1 -1
  37. package/dist/lib/huggingface-provider.d.ts.map +1 -1
  38. package/dist/lib/huggingface-provider.js +11 -3
  39. package/dist/lib/huggingface-provider.js.map +1 -1
  40. package/dist/lib/license.d.ts +76 -0
  41. package/dist/lib/license.d.ts.map +1 -0
  42. package/dist/lib/license.js +141 -0
  43. package/dist/lib/license.js.map +1 -0
  44. package/docs/agentic-harness-primer.md +283 -0
  45. package/docs/conditional-continuation.md +167 -0
  46. package/docs/web-app-agent-guide.md +520 -0
  47. package/examples/web-app-agent/README.md +187 -0
  48. package/examples/web-app-agent/agent/mcp-shim.js +105 -0
  49. package/examples/web-app-agent/gateway.config.example.json +52 -0
  50. package/examples/web-app-agent/package.json +13 -0
  51. package/examples/web-app-agent/public/agent/bridge-mount.js +75 -0
  52. package/examples/web-app-agent/public/agent/chat-client.js +104 -0
  53. package/examples/web-app-agent/public/agent/commands.js +250 -0
  54. package/examples/web-app-agent/public/agent/device-thread.js +48 -0
  55. package/examples/web-app-agent/public/agent/panel.css +107 -0
  56. package/examples/web-app-agent/public/agent/panel.js +369 -0
  57. package/examples/web-app-agent/public/app.js +250 -0
  58. package/examples/web-app-agent/public/index.html +111 -0
  59. package/examples/web-app-agent/server.js +242 -0
  60. 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; }',
@@ -1197,6 +1240,12 @@
1197
1240
  ' text-decoration: underline; text-underline-offset: 2px;',
1198
1241
  '}',
1199
1242
  '.cumulus-topbar-changelog:hover { color: #ccc; background: #3a3a3a; }',
1243
+ '.cumulus-topbar-settings {',
1244
+ ' background: none; border: none; color: #888; cursor: pointer;',
1245
+ ' font-size: 15px; padding: 2px 5px; line-height: 1;',
1246
+ ' border-radius: 3px; transition: color 0.2s, background 0.2s;',
1247
+ '}',
1248
+ '.cumulus-topbar-settings:hover { color: #ccc; background: #3a3a3a; }',
1200
1249
  '.cumulus-changelog-popover {',
1201
1250
  ' position: absolute; top: 44px; left: 12px; z-index: 1000;',
1202
1251
  ' width: min(480px, calc(100vw - 24px)); max-height: 70vh; overflow-y: auto;',
@@ -1315,137 +1364,188 @@
1315
1364
  '.cumulus-model-popover select { max-width: 152px; margin: 0; }',
1316
1365
 
1317
1366
  /* ── Settings modal (provider-organized model catalog + credentials) ── */
1367
+ /* These three roots mount on document.body, OUTSIDE .cumulus-widget, so they
1368
+ must carry the font stack themselves or the browser falls back to serif. */
1369
+ '.cumulus-settings-backdrop, .cumulus-context-menu, .cumulus-confirm-dialog {',
1370
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
1371
+ ' -webkit-font-smoothing: antialiased;',
1372
+ '}',
1318
1373
  '.cumulus-settings-backdrop {',
1319
- ' position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.55);',
1374
+ ' position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.6);',
1375
+ ' backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);',
1320
1376
  ' display: flex; align-items: center; justify-content: center; padding: 20px;',
1377
+ ' font-size: 13px; line-height: 1.5; color: #e3e5ea;',
1321
1378
  '}',
1322
1379
  '.cumulus-settings-modal {',
1323
- ' background: #232323; border: 1px solid #3d3d3d; border-radius: 10px;',
1380
+ ' background: #1f2023; border: 1px solid #313338; border-radius: 14px;',
1324
1381
  ' 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;',
1382
+ ' box-shadow: 0 24px 64px rgba(0,0,0,0.55); overflow: hidden;',
1326
1383
  '}',
1327
1384
  '.cumulus-settings-header {',
1328
1385
  ' display: flex; align-items: flex-start; justify-content: space-between;',
1329
- ' padding: 14px 18px; border-bottom: 1px solid #333; flex-shrink: 0;',
1386
+ ' padding: 16px 20px 14px; border-bottom: 1px solid #2a2b30; flex-shrink: 0;',
1330
1387
  '}',
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; }',
1388
+ '.cumulus-settings-title { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; color: #f2f3f5; }',
1389
+ '.cumulus-settings-subtitle { font-size: 12px; color: #8b8f96; margin-top: 3px; }',
1390
+ '.cumulus-settings-body { padding: 6px 20px 14px; overflow-y: auto; flex: 1; }',
1334
1391
  '.cumulus-settings-footer {',
1335
- ' display: flex; align-items: center; gap: 12px; padding: 13px 18px;',
1336
- ' border-top: 1px solid #333; flex-shrink: 0;',
1392
+ ' display: flex; align-items: center; gap: 12px; padding: 13px 20px;',
1393
+ ' border-top: 1px solid #2a2b30; flex-shrink: 0; background: #1b1c1f;',
1337
1394
  '}',
1338
1395
  '.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;',
1396
+ ' font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;',
1397
+ ' color: #6e7278; margin: 18px 0 8px;',
1341
1398
  '}',
1342
- '.cumulus-settings-body > .cumulus-settings-section-label:first-child { margin-top: 6px; }',
1399
+ '.cumulus-settings-body > .cumulus-settings-section-label:first-child { margin-top: 12px; }',
1400
+ '.cumulus-settings-default-card {',
1401
+ ' display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin: 4px 0 2px;',
1402
+ ' border: 1px solid rgba(79,140,255,0.35); border-radius: 12px; background: rgba(79,140,255,0.07);',
1403
+ '}',
1404
+ '.cumulus-settings-default-card label { color: #c8d4e8; font-size: 12.5px; font-weight: 500; flex-shrink: 0; }',
1405
+ '.cumulus-settings-default-card select { flex: 1; min-width: 0; }',
1343
1406
  '.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;',
1407
+ ' display: flex; align-items: center; gap: 10px; padding: 9px 12px;',
1408
+ ' border: 1px solid #2c2d32; border-radius: 10px; margin-bottom: 6px; background: #1e1f23;',
1409
+ ' transition: border-color 0.12s;',
1347
1410
  '}',
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; }',
1411
+ '.cumulus-settings-model-row:hover { border-color: #3a3c42; }',
1350
1412
  '.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; }',
1413
+ '.cumulus-settings-model-row.is-default { border-color: rgba(79,140,255,0.45); background: rgba(79,140,255,0.08); }',
1354
1414
  '.cumulus-settings-model-name { flex: 1; min-width: 0; }',
1355
1415
  '.cumulus-settings-model-label {',
1356
- ' color: #e6e6e6; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
1416
+ ' color: #e7e8ea; font-size: 13px; font-weight: 500;',
1417
+ ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
1357
1418
  '}',
1358
1419
  '.cumulus-settings-model-id {',
1359
- ' color: #777; font-size: 11px; font-family: "SF Mono", Consolas, monospace;',
1420
+ ' color: #7d808a; font-size: 11px;',
1421
+ ' font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;',
1360
1422
  ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px;',
1361
1423
  '}',
1424
+ '.cumulus-settings-context-chip {',
1425
+ ' color: #9a9da5; font-size: 10.5px; font-variant-numeric: tabular-nums;',
1426
+ ' border: 1px solid #383a40; border-radius: 999px; background: #1b1c1f;',
1427
+ ' padding: 1px 8px; white-space: nowrap; flex-shrink: 0;',
1428
+ '}',
1362
1429
  '.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;',
1430
+ ' display: none; font-size: 10.5px; font-weight: 600; color: #8ab4f8;',
1431
+ ' background: rgba(79,140,255,0.14); border: 1px solid rgba(79,140,255,0.35);',
1432
+ ' border-radius: 999px; padding: 1px 9px; flex-shrink: 0;',
1366
1433
  '}',
1367
1434
  '.cumulus-settings-model-row.is-default .cumulus-settings-default-badge { display: inline-block; }',
1435
+ '.cumulus-settings-row-actions {',
1436
+ ' display: flex; align-items: center; gap: 2px; flex-shrink: 0; opacity: 0;',
1437
+ ' transition: opacity 0.12s;',
1438
+ '}',
1439
+ '.cumulus-settings-model-row:hover .cumulus-settings-row-actions,',
1440
+ '.cumulus-settings-model-row:focus-within .cumulus-settings-row-actions { opacity: 1; }',
1441
+ '@media (hover: none) { .cumulus-settings-row-actions { opacity: 1; } }',
1442
+ '.cumulus-settings-row-edit {',
1443
+ ' background: none; border: none; color: #6e7278; cursor: pointer; font-size: 13px;',
1444
+ ' line-height: 1; padding: 2px 4px; flex-shrink: 0; font-family: inherit;',
1445
+ '}',
1446
+ '.cumulus-settings-row-edit:hover { color: #8ab4f8; }',
1368
1447
  '.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;',
1448
+ ' background: none; border: none; color: #6e7278; cursor: pointer; font-size: 17px;',
1449
+ ' line-height: 1; padding: 0 2px; flex-shrink: 0; font-family: inherit;',
1371
1450
  '}',
1372
1451
  '.cumulus-settings-model-remove:hover { color: #ef4444; }',
1452
+ '.cumulus-settings-model-row.editing { display: block; }',
1453
+ '.cumulus-settings-edit-grid {',
1454
+ ' display: grid; grid-template-columns: minmax(140px,1.3fr) minmax(110px,1fr) 105px auto;',
1455
+ ' gap: 8px; align-items: end;',
1456
+ '}',
1457
+ '.cumulus-settings-edit-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }',
1458
+ '.cumulus-settings-edit-field > span {',
1459
+ ' color: #6e7278; font-size: 10px; font-weight: 600;',
1460
+ ' text-transform: uppercase; letter-spacing: 0.05em;',
1461
+ '}',
1462
+ '.cumulus-settings-claude-default-row {',
1463
+ ' display: flex; align-items: center; gap: 10px; padding: 4px 12px 10px;',
1464
+ '}',
1465
+ '.cumulus-settings-claude-default-row label { color: #9a9da5; font-size: 11.5px; flex-shrink: 0; }',
1466
+ '.cumulus-settings-claude-default-row select { flex: 1; min-width: 0; }',
1373
1467
  '.cumulus-settings-provider-card {',
1374
- ' border: 1px solid #363636; border-radius: 10px; background: #1d1d1d; margin: 10px 0;',
1468
+ ' border: 1px solid #2c2d32; border-radius: 12px; background: #232428; margin: 10px 0;',
1375
1469
  ' overflow: hidden;',
1376
1470
  '}',
1377
1471
  '.cumulus-settings-provider-head {',
1378
1472
  ' display: flex; align-items: center; justify-content: space-between; gap: 14px;',
1379
- ' padding: 12px 13px; background: #202020; border-bottom: 1px solid #323232;',
1473
+ ' padding: 13px 14px; background: #26272c; border-bottom: 1px solid #2c2d32;',
1380
1474
  '}',
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; }',
1475
+ '.cumulus-settings-provider-title { color: #f2f3f5; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }',
1476
+ '.cumulus-settings-provider-desc { color: #7d808a; font-size: 11.5px; margin-top: 2px; }',
1383
1477
  '.cumulus-settings-provider-count {',
1384
- ' color: #999; font-size: 10px; border: 1px solid #3c3c3c; border-radius: 999px;',
1385
- ' padding: 2px 8px; white-space: nowrap;',
1478
+ ' color: #9a9da5; font-size: 10.5px; font-variant-numeric: tabular-nums;',
1479
+ ' border: 1px solid #383a40; border-radius: 999px; background: #1b1c1f;',
1480
+ ' padding: 2px 9px; white-space: nowrap;',
1386
1481
  '}',
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; }',
1482
+ '.cumulus-settings-provider-models { padding: 10px 10px 4px; }',
1483
+ '.cumulus-settings-provider-models .cumulus-settings-model-row { margin-bottom: 6px; }',
1484
+ '.cumulus-settings-provider-empty { color: #6e7278; font-size: 12px; padding: 8px 4px 14px; }',
1390
1485
  '.cumulus-settings-provider-credential {',
1391
1486
  ' display: grid; grid-template-columns: 120px minmax(0,1fr) auto; align-items: center; gap: 10px;',
1392
- ' padding: 0 10px 11px;',
1487
+ ' padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1488
+ '}',
1489
+ '.cumulus-settings-credential-note {',
1490
+ ' color: #7d808a; font-size: 11.5px; padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
1393
1491
  '}',
1394
- '.cumulus-settings-credential-note { color: #777; font-size: 11px; padding: 0 10px 12px; }',
1395
1492
  '.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;',
1493
+ ' background: transparent; border: 1px solid #3d3f45; color: #9a9da5; cursor: pointer;',
1494
+ ' padding: 6px 10px; border-radius: 8px; font-size: 11.5px; font-family: inherit;',
1495
+ ' transition: color 0.12s, border-color 0.12s;',
1398
1496
  '}',
1399
- '.cumulus-settings-clear-btn:hover { color: #ef7777; border-color: #744; }',
1400
- '.cumulus-settings-provider-credential label { color: #aaa; font-size: 11px; }',
1497
+ '.cumulus-settings-clear-btn:hover { color: #ef7777; border-color: #7a4444; }',
1498
+ '.cumulus-settings-provider-credential label { color: #9a9da5; font-size: 11.5px; }',
1401
1499
  '.cumulus-settings-add-card {',
1402
- ' border: 1px dashed #454545; border-radius: 10px; padding: 12px; margin: 12px 0 8px;',
1500
+ ' border: 1px dashed #3d3f45; border-radius: 12px; padding: 13px 14px; margin: 12px 0 8px;',
1403
1501
  ' background: rgba(255,255,255,0.015);',
1404
1502
  '}',
1405
- '.cumulus-settings-add-title { color: #ddd; font-size: 12px; font-weight: 650; margin-bottom: 9px; }',
1503
+ '.cumulus-settings-add-title { color: #d4d6da; font-size: 12px; font-weight: 600; margin-bottom: 9px; }',
1406
1504
  '.cumulus-settings-add-row {',
1407
1505
  ' display: grid; grid-template-columns: 130px minmax(150px,1.4fr) minmax(120px,1fr) 110px auto;',
1408
- ' gap: 7px; align-items: center;',
1506
+ ' gap: 8px; align-items: center;',
1507
+ '}',
1508
+ '.cumulus-settings-select, .cumulus-settings-input {',
1509
+ ' background: #17181b; border: 1px solid #33353b; color: #e3e5ea;',
1510
+ ' padding: 7px 10px; border-radius: 8px; font-size: 12.5px; font-family: inherit; outline: none;',
1511
+ ' transition: border-color 0.12s, box-shadow 0.12s;',
1409
1512
  '}',
1410
1513
  '.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;',
1514
+ ' -webkit-appearance: none; appearance: none; padding-right: 26px; cursor: pointer;',
1515
+ ' 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");',
1516
+ ' background-repeat: no-repeat; background-position: right 10px center;',
1413
1517
  '}',
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;',
1518
+ '.cumulus-settings-select:focus, .cumulus-settings-input:focus {',
1519
+ ' border-color: #4f8cff; box-shadow: 0 0 0 3px rgba(79,140,255,0.18);',
1417
1520
  '}',
1418
- '.cumulus-settings-input:focus { border-color: #3b82f6; }',
1419
- '.cumulus-settings-input::placeholder { color: #5a5a5a; }',
1521
+ '.cumulus-settings-input::placeholder { color: #5c5f66; }',
1420
1522
  '.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; }',
1523
+ '.cumulus-settings-cred-row label { font-size: 12px; color: #b8bac0; flex-shrink: 0; }',
1422
1524
  '.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;',
1525
+ ' background: #3b82f6; border: none; color: #fff; font-weight: 600;',
1526
+ ' padding: 8px 20px; border-radius: 8px; font-size: 13px; cursor: pointer; font-family: inherit;',
1527
+ ' transition: background 0.12s;',
1425
1528
  '}',
1426
- '.cumulus-settings-btn:hover { background: #3a7ae0; }',
1427
- '.cumulus-settings-btn:disabled { background: #333; color: #777; cursor: not-allowed; }',
1529
+ '.cumulus-settings-btn:hover { background: #2f6fe0; }',
1530
+ '.cumulus-settings-btn:disabled { background: #2c2d32; color: #6e7278; cursor: not-allowed; }',
1428
1531
  '.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;',
1532
+ ' background: transparent; border: 1px solid #3d3f45; color: #d4d6da;',
1533
+ ' padding: 7px 14px; border-radius: 8px; font-size: 12.5px; cursor: pointer;',
1534
+ ' font-family: inherit; white-space: nowrap; transition: background 0.12s, border-color 0.12s;',
1432
1535
  '}',
1433
- '.cumulus-settings-add-btn:hover { background: #3a3a3a; border-color: #555; }',
1434
- '.cumulus-settings-status { font-size: 12px; color: #999; }',
1536
+ '.cumulus-settings-add-btn:hover { background: #2a2b30; border-color: #4a4c53; }',
1537
+ '.cumulus-settings-status { font-size: 12px; color: #9a9da5; }',
1435
1538
  '.cumulus-settings-status.success { color: #22c55e; }',
1436
1539
  '.cumulus-settings-status.error { color: #ef4444; }',
1437
1540
  '@media (max-width: 720px) {',
1438
1541
  ' .cumulus-settings-backdrop { padding: 8px; align-items: stretch; }',
1439
- ' .cumulus-settings-modal { max-height: 100%; border-radius: 8px; }',
1542
+ ' .cumulus-settings-modal { max-height: 100%; border-radius: 12px; }',
1440
1543
  ' .cumulus-settings-add-row { grid-template-columns: 1fr 1fr; }',
1441
1544
  ' .cumulus-settings-add-row .cumulus-settings-add-btn { grid-column: 1 / -1; }',
1442
1545
  ' .cumulus-settings-provider-credential { grid-template-columns: 1fr auto; gap: 5px; }',
1443
1546
  ' .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; }',
1547
+ ' .cumulus-settings-default-card { flex-direction: column; align-items: stretch; gap: 6px; }',
1548
+ ' .cumulus-settings-edit-grid { grid-template-columns: 1fr 1fr; }',
1449
1549
  '}',
1450
1550
 
1451
1551
  /* ── Context menu (right-click on thread) ── */
@@ -4500,6 +4600,7 @@
4500
4600
  '<button class="cumulus-topbar-check-update" data-testid="webchat-check-update" title="Check for update">&#8635;</button>' +
4501
4601
  '<span class="cumulus-topbar-check-result" data-testid="webchat-check-result" style="display:none"></span></span>' +
4502
4602
  '<div class="cumulus-topbar-right">' +
4603
+ '<button class="cumulus-topbar-settings" data-testid="webchat-settings-btn" title="Gateway settings — models, providers, API keys">&#9881;</button>' +
4503
4604
  '<span class="cumulus-header-status">' +
4504
4605
  '<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
4505
4606
  '<span data-testid="webchat-status-text-app">Disconnected</span>' +
@@ -4689,6 +4790,14 @@
4689
4790
  });
4690
4791
  }
4691
4792
 
4793
+ // ── Topbar settings gear → gateway settings modal (task 128) ──
4794
+ var topbarSettingsBtn = topbar.querySelector('[data-testid="webchat-settings-btn"]');
4795
+ if (topbarSettingsBtn) {
4796
+ topbarSettingsBtn.addEventListener('click', function () {
4797
+ openSettingsModal();
4798
+ });
4799
+ }
4800
+
4692
4801
  // ── Click status indicator to force reconnect ──
4693
4802
  var appStatusEl = topbar.querySelector('.cumulus-header-status');
4694
4803
  if (appStatusEl) {
@@ -5846,6 +5955,715 @@
5846
5955
  };
5847
5956
  }
5848
5957
 
5958
+ // ── Settings modal (provider-organized model catalog + credentials) ──
5959
+ // Live-editable via PUT /api/config (task 104) — takes effect on the NEXT turn
5960
+ // with no restart, no JSON edit, no republish. Any valid gateway key.
5961
+ // Gateway-level config, opened from the topbar gear next to the connection
5962
+ // status — not tied to any thread panel (task 128).
5963
+ function openSettingsModal() {
5964
+ var backdrop = document.createElement('div');
5965
+ backdrop.className = 'cumulus-settings-backdrop';
5966
+ backdrop.setAttribute('data-testid', 'webchat-settings-modal');
5967
+ var modal = document.createElement('div');
5968
+ modal.className = 'cumulus-settings-modal';
5969
+
5970
+ var mHeader = document.createElement('div');
5971
+ mHeader.className = 'cumulus-settings-header';
5972
+ var mHeaderText = document.createElement('div');
5973
+ var mTitle = document.createElement('div');
5974
+ mTitle.className = 'cumulus-settings-title';
5975
+ mTitle.textContent = 'Gateway settings';
5976
+ var mSub = document.createElement('div');
5977
+ mSub.className = 'cumulus-settings-subtitle';
5978
+ mSub.textContent =
5979
+ 'Manage Anthropic, OpenAI, and HuggingFace models — changes apply to new turns instantly';
5980
+ mHeaderText.appendChild(mTitle);
5981
+ mHeaderText.appendChild(mSub);
5982
+ var mClose = document.createElement('button');
5983
+ mClose.className = 'cumulus-overlay-close';
5984
+ mClose.textContent = '\xD7';
5985
+ mClose.setAttribute('data-testid', 'webchat-settings-close');
5986
+ mClose.addEventListener('click', function () {
5987
+ backdrop.remove();
5988
+ });
5989
+ mHeader.appendChild(mHeaderText);
5990
+ mHeader.appendChild(mClose);
5991
+ modal.appendChild(mHeader);
5992
+
5993
+ var body = document.createElement('div');
5994
+ body.className = 'cumulus-settings-body';
5995
+
5996
+ // Task 130 (Option C): ONE gateway-default control, up top — answers "what
5997
+ // will a new thread use?" before the catalog. The per-row radios are gone.
5998
+ var defaultLabelSection = document.createElement('div');
5999
+ defaultLabelSection.className = 'cumulus-settings-section-label';
6000
+ defaultLabelSection.textContent = 'Default model';
6001
+ body.appendChild(defaultLabelSection);
6002
+ var defaultCard = document.createElement('div');
6003
+ defaultCard.className = 'cumulus-settings-default-card';
6004
+ var defaultCardLabel = document.createElement('label');
6005
+ defaultCardLabel.textContent = 'New threads use';
6006
+ var defaultSelect = document.createElement('select');
6007
+ defaultSelect.className = 'cumulus-settings-select';
6008
+ defaultSelect.setAttribute('data-testid', 'webchat-gw-default-select');
6009
+ defaultSelect.addEventListener('change', function () {
6010
+ applyGatewayDefault(gwState.catalog, defaultSelect.value);
6011
+ renderAll();
6012
+ });
6013
+ defaultCard.appendChild(defaultCardLabel);
6014
+ defaultCard.appendChild(defaultSelect);
6015
+ body.appendChild(defaultCard);
6016
+
6017
+ var modelsLabel = document.createElement('div');
6018
+ modelsLabel.className = 'cumulus-settings-section-label';
6019
+ modelsLabel.textContent = 'Model catalog';
6020
+ body.appendChild(modelsLabel);
6021
+
6022
+ var gwList = document.createElement('div');
6023
+ gwList.setAttribute('data-testid', 'webchat-gateway-models');
6024
+ body.appendChild(gwList);
6025
+
6026
+ var addCard = document.createElement('div');
6027
+ addCard.className = 'cumulus-settings-add-card';
6028
+ var addTitle = document.createElement('div');
6029
+ addTitle.className = 'cumulus-settings-add-title';
6030
+ addTitle.textContent = 'Add a model';
6031
+ addCard.appendChild(addTitle);
6032
+ var addWrap = document.createElement('div');
6033
+ addWrap.className = 'cumulus-settings-add-row';
6034
+ var addProvider = document.createElement('select');
6035
+ addProvider.className = 'cumulus-settings-select';
6036
+ addProvider.setAttribute('data-testid', 'webchat-gw-add-provider');
6037
+ [
6038
+ { value: 'anthropic', label: 'Anthropic' },
6039
+ { value: 'openai', label: 'OpenAI' },
6040
+ { value: 'huggingface', label: 'HuggingFace' },
6041
+ ].forEach(function (provider) {
6042
+ var option = document.createElement('option');
6043
+ option.value = provider.value;
6044
+ option.textContent = provider.label;
6045
+ addProvider.appendChild(option);
6046
+ });
6047
+ var addId = document.createElement('input');
6048
+ addId.type = 'text';
6049
+ addId.className = 'cumulus-settings-input';
6050
+ addId.placeholder = 'Model ID / name';
6051
+ addId.setAttribute('data-testid', 'webchat-gw-add-id');
6052
+ var addLabel = document.createElement('input');
6053
+ addLabel.type = 'text';
6054
+ addLabel.className = 'cumulus-settings-input';
6055
+ addLabel.placeholder = 'Display label (optional)';
6056
+ addLabel.setAttribute('data-testid', 'webchat-gw-add-label');
6057
+ var addContext = document.createElement('input');
6058
+ addContext.type = 'number';
6059
+ addContext.min = '1';
6060
+ addContext.step = '1';
6061
+ addContext.className = 'cumulus-settings-input';
6062
+ addContext.placeholder = 'Context tokens';
6063
+ addContext.setAttribute('data-testid', 'webchat-gw-add-context');
6064
+ var addBtn = document.createElement('button');
6065
+ addBtn.type = 'button';
6066
+ addBtn.textContent = '+ Add';
6067
+ addBtn.className = 'cumulus-settings-add-btn';
6068
+ addBtn.setAttribute('data-testid', 'webchat-gw-add-btn');
6069
+ addWrap.appendChild(addProvider);
6070
+ addWrap.appendChild(addId);
6071
+ addWrap.appendChild(addLabel);
6072
+ addWrap.appendChild(addContext);
6073
+ addWrap.appendChild(addBtn);
6074
+ addCard.appendChild(addWrap);
6075
+ body.appendChild(addCard);
6076
+
6077
+ // ── License (task 129) — persist-only: the key lands in gateway.config.json
6078
+ // but enforcement re-verifies at startup, so it takes effect on reload (127).
6079
+ var licenseLabel = document.createElement('div');
6080
+ licenseLabel.className = 'cumulus-settings-section-label';
6081
+ licenseLabel.textContent = 'License';
6082
+ body.appendChild(licenseLabel);
6083
+ var licenseCard = document.createElement('div');
6084
+ licenseCard.className = 'cumulus-settings-add-card';
6085
+ var licenseStatusLine = document.createElement('div');
6086
+ licenseStatusLine.className = 'cumulus-settings-add-title';
6087
+ licenseStatusLine.setAttribute('data-testid', 'webchat-gw-license-status');
6088
+ licenseStatusLine.textContent = 'License status unknown';
6089
+ licenseCard.appendChild(licenseStatusLine);
6090
+ var licensePurpose = document.createElement('div');
6091
+ licensePurpose.className = 'cumulus-settings-subtitle';
6092
+ licensePurpose.setAttribute('data-testid', 'webchat-gw-license-purpose');
6093
+ licensePurpose.textContent =
6094
+ 'Required to run web-app agents in production. Unlicensed, each thread ' +
6095
+ 'namespace is capped at 5 visitor threads (demo mode); your own threads, ' +
6096
+ 'the CLI and the TUI are never limited. Keys are verified offline and ' +
6097
+ 'cover one whole-number release. Contact ops@luckydrawdesign.com.';
6098
+ licenseCard.appendChild(licensePurpose);
6099
+ var licenseRow = document.createElement('div');
6100
+ licenseRow.className = 'cumulus-settings-add-row';
6101
+ var licenseInput = document.createElement('input');
6102
+ licenseInput.type = 'text';
6103
+ licenseInput.className = 'cumulus-settings-input';
6104
+ licenseInput.placeholder = 'License key (cumulus-lic-v1.…)';
6105
+ licenseInput.setAttribute('data-testid', 'webchat-gw-license-input');
6106
+ var licenseClearBtn = document.createElement('button');
6107
+ licenseClearBtn.type = 'button';
6108
+ licenseClearBtn.textContent = 'Clear';
6109
+ licenseClearBtn.className = 'cumulus-settings-add-btn';
6110
+ licenseClearBtn.setAttribute('data-testid', 'webchat-gw-license-clear');
6111
+ licenseRow.appendChild(licenseInput);
6112
+ licenseRow.appendChild(licenseClearBtn);
6113
+ licenseCard.appendChild(licenseRow);
6114
+ var licenseNote = document.createElement('div');
6115
+ licenseNote.className = 'cumulus-settings-subtitle';
6116
+ licenseNote.textContent = 'Takes effect on gateway reload';
6117
+ licenseCard.appendChild(licenseNote);
6118
+ body.appendChild(licenseCard);
6119
+ modal.appendChild(body);
6120
+
6121
+ var footer = document.createElement('div');
6122
+ footer.className = 'cumulus-settings-footer';
6123
+ var saveBtn = document.createElement('button');
6124
+ saveBtn.textContent = 'Save';
6125
+ saveBtn.className = 'cumulus-settings-btn';
6126
+ saveBtn.setAttribute('data-testid', 'webchat-gw-save');
6127
+ var gwStatus = document.createElement('span');
6128
+ gwStatus.className = 'cumulus-settings-status';
6129
+ footer.appendChild(saveBtn);
6130
+ footer.appendChild(gwStatus);
6131
+ modal.appendChild(footer);
6132
+
6133
+ backdrop.appendChild(modal);
6134
+ backdrop.addEventListener('click', function (e) {
6135
+ if (e.target === backdrop) backdrop.remove();
6136
+ });
6137
+ document.body.appendChild(backdrop);
6138
+
6139
+ var gwState = { catalog: [], sentinel: null };
6140
+ var credentialInputs = {};
6141
+ var credentialValues = { openai: '', huggingface: '', license: '' };
6142
+ var credentialClears = { openai: false, huggingface: false, license: false };
6143
+ function renderLicenseState(masked, info) {
6144
+ licenseInput.value = credentialValues.license || '';
6145
+ licenseInput.placeholder = credentialClears.license
6146
+ ? 'Will be cleared on Save'
6147
+ : masked || 'License key (cumulus-lic-v1.…)';
6148
+ if (!info) {
6149
+ licenseStatusLine.textContent = masked
6150
+ ? 'License key configured'
6151
+ : 'No license key — demo mode';
6152
+ return;
6153
+ }
6154
+ var text = info.licensed
6155
+ ? 'Licensed to ' + (info.licensee || 'unknown')
6156
+ : 'Demo mode' + (info.reason && info.reason !== 'absent' ? ' (' + info.reason + ')' : '');
6157
+ if (info.pendingReload) text += ' · key saved — takes effect on reload';
6158
+ licenseStatusLine.textContent = text;
6159
+ }
6160
+ licenseInput.addEventListener('input', function () {
6161
+ credentialValues.license = licenseInput.value;
6162
+ credentialClears.license = false;
6163
+ });
6164
+ licenseClearBtn.addEventListener('click', function () {
6165
+ credentialClears.license = true;
6166
+ credentialValues.license = '';
6167
+ renderLicenseState('', gwLicenseInfo);
6168
+ });
6169
+ var gwLicenseMasked = '';
6170
+ var gwLicenseInfo = null;
6171
+ var providerDefs = [
6172
+ {
6173
+ id: 'anthropic',
6174
+ title: 'Anthropic',
6175
+ description: 'Claude CLI models · authentication is managed by the Claude CLI',
6176
+ },
6177
+ {
6178
+ id: 'openai',
6179
+ title: 'OpenAI',
6180
+ description: 'Responses API models with direct tool use',
6181
+ credential: 'openaiApiKey',
6182
+ credentialTestId: 'webchat-gw-openaikey',
6183
+ },
6184
+ {
6185
+ id: 'huggingface',
6186
+ title: 'HuggingFace',
6187
+ description: 'OpenAI-compatible models served through the HuggingFace router',
6188
+ credential: 'hfApiKey',
6189
+ credentialTestId: 'webchat-gw-hfkey',
6190
+ },
6191
+ ];
6192
+
6193
+ function setSettingsStatus(message, kind) {
6194
+ gwStatus.textContent = message || '';
6195
+ gwStatus.className = 'cumulus-settings-status' + (kind ? ' ' + kind : '');
6196
+ }
6197
+
6198
+ function formatContextTokens(n) {
6199
+ return n >= 1000 ? Math.round(n / 1000) + 'k' : String(n);
6200
+ }
6201
+
6202
+ function makeEditField(labelText, input) {
6203
+ var field = document.createElement('div');
6204
+ field.className = 'cumulus-settings-edit-field';
6205
+ var caption = document.createElement('span');
6206
+ caption.textContent = labelText;
6207
+ field.appendChild(caption);
6208
+ field.appendChild(input);
6209
+ return field;
6210
+ }
6211
+
6212
+ // Task 130: rows are read-only text at rest (label + mono id + ctx chip +
6213
+ // default badge); ✎ flips one row into labeled inputs. `model.editing` is
6214
+ // UI-only state — serialization rebuilds entries from `model.source` +
6215
+ // explicit fields, so it never reaches the PUT payload.
6216
+ function renderModelRow(model, providerDef, index) {
6217
+ var row = document.createElement('div');
6218
+ row.className =
6219
+ 'cumulus-settings-model-row ' +
6220
+ model.provider +
6221
+ (model.gatewayDefault ? ' is-default' : '') +
6222
+ (model.editing ? ' editing' : '');
6223
+ row.setAttribute('data-gw-model', model.id);
6224
+ row.setAttribute('data-testid', 'webchat-gw-row-' + model.provider + '-' + index);
6225
+
6226
+ if (model.editing) {
6227
+ var grid = document.createElement('div');
6228
+ grid.className = 'cumulus-settings-edit-grid';
6229
+
6230
+ var idInput = document.createElement('input');
6231
+ idInput.type = 'text';
6232
+ idInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6233
+ idInput.value = model.id;
6234
+ idInput.setAttribute('aria-label', providerDef.title + ' model ID');
6235
+ idInput.setAttribute('data-testid', 'webchat-gw-id-' + model.provider + '-' + index);
6236
+ idInput.addEventListener('input', function () {
6237
+ model.id = idInput.value;
6238
+ });
6239
+ grid.appendChild(makeEditField('Model ID', idInput));
6240
+
6241
+ var labelInput = document.createElement('input');
6242
+ labelInput.type = 'text';
6243
+ labelInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6244
+ labelInput.value = model.label || model.id;
6245
+ labelInput.setAttribute('aria-label', providerDef.title + ' display label');
6246
+ labelInput.setAttribute(
6247
+ 'data-testid',
6248
+ 'webchat-gw-label-' + model.provider + '-' + index
6249
+ );
6250
+ labelInput.addEventListener('input', function () {
6251
+ model.label = labelInput.value;
6252
+ });
6253
+ grid.appendChild(makeEditField('Display label', labelInput));
6254
+
6255
+ if (model.provider !== 'anthropic') {
6256
+ var contextInput = document.createElement('input');
6257
+ contextInput.type = 'number';
6258
+ contextInput.min = '1';
6259
+ contextInput.step = '1';
6260
+ contextInput.className = 'cumulus-settings-input cumulus-settings-row-input';
6261
+ contextInput.placeholder = '(default)';
6262
+ contextInput.value = model.contextWindow || '';
6263
+ contextInput.setAttribute('aria-label', providerDef.title + ' context window');
6264
+ contextInput.setAttribute(
6265
+ 'data-testid',
6266
+ 'webchat-gw-context-' + model.provider + '-' + index
6267
+ );
6268
+ contextInput.addEventListener('input', function () {
6269
+ model.contextWindow = contextInput.value ? Number(contextInput.value) : undefined;
6270
+ });
6271
+ grid.appendChild(makeEditField('Context tokens', contextInput));
6272
+ }
6273
+
6274
+ var doneBtn = document.createElement('button');
6275
+ doneBtn.type = 'button';
6276
+ doneBtn.textContent = 'Done';
6277
+ doneBtn.className = 'cumulus-settings-add-btn';
6278
+ doneBtn.setAttribute('data-testid', 'webchat-gw-done-' + model.provider + '-' + index);
6279
+ doneBtn.addEventListener('click', function () {
6280
+ model.editing = false;
6281
+ renderAll();
6282
+ });
6283
+ grid.appendChild(doneBtn);
6284
+ row.appendChild(grid);
6285
+ return row;
6286
+ }
6287
+
6288
+ var name = document.createElement('div');
6289
+ name.className = 'cumulus-settings-model-name';
6290
+ var labelEl = document.createElement('div');
6291
+ labelEl.className = 'cumulus-settings-model-label';
6292
+ labelEl.textContent = model.label || model.id;
6293
+ var idEl = document.createElement('div');
6294
+ idEl.className = 'cumulus-settings-model-id';
6295
+ idEl.textContent = model.id;
6296
+ name.appendChild(labelEl);
6297
+ name.appendChild(idEl);
6298
+ row.appendChild(name);
6299
+
6300
+ if (model.provider !== 'anthropic' && model.contextWindow) {
6301
+ var chip = document.createElement('span');
6302
+ chip.className = 'cumulus-settings-context-chip';
6303
+ chip.textContent = 'ctx ' + formatContextTokens(model.contextWindow);
6304
+ row.appendChild(chip);
6305
+ }
6306
+
6307
+ var badge = document.createElement('span');
6308
+ badge.className = 'cumulus-settings-default-badge';
6309
+ badge.textContent = 'default';
6310
+ row.appendChild(badge);
6311
+
6312
+ var actions = document.createElement('div');
6313
+ actions.className = 'cumulus-settings-row-actions';
6314
+
6315
+ var edit = document.createElement('button');
6316
+ edit.type = 'button';
6317
+ edit.className = 'cumulus-settings-row-edit';
6318
+ edit.textContent = '✎';
6319
+ edit.title = 'Edit ' + model.id;
6320
+ edit.setAttribute('data-testid', 'webchat-gw-edit-' + model.provider + '-' + index);
6321
+ edit.addEventListener('click', function () {
6322
+ model.editing = true;
6323
+ renderAll();
6324
+ });
6325
+ actions.appendChild(edit);
6326
+
6327
+ var remove = document.createElement('button');
6328
+ remove.type = 'button';
6329
+ remove.className = 'cumulus-settings-model-remove';
6330
+ remove.textContent = '\xD7';
6331
+ remove.title = 'Remove ' + model.id;
6332
+ remove.setAttribute('data-testid', 'webchat-gw-remove-' + model.provider + '-' + index);
6333
+ remove.addEventListener('click', function () {
6334
+ var absoluteIndex = gwState.catalog.indexOf(model);
6335
+ var wasGatewayDefault = model.gatewayDefault;
6336
+ var wasProviderDefault = model.providerDefault;
6337
+ if (absoluteIndex >= 0) gwState.catalog.splice(absoluteIndex, 1);
6338
+ if (wasGatewayDefault && gwState.catalog.length) {
6339
+ applyGatewayDefault(gwState.catalog, gwState.catalog[0].id);
6340
+ }
6341
+ if (
6342
+ wasProviderDefault &&
6343
+ !gwState.catalog.some(function (item) {
6344
+ return item.provider === 'anthropic' && item.providerDefault;
6345
+ })
6346
+ ) {
6347
+ var nextClaude = gwState.catalog.find(function (item) {
6348
+ return item.provider === 'anthropic';
6349
+ });
6350
+ if (nextClaude) nextClaude.providerDefault = true;
6351
+ }
6352
+ renderAll();
6353
+ });
6354
+ actions.appendChild(remove);
6355
+ row.appendChild(actions);
6356
+ return row;
6357
+ }
6358
+
6359
+ function renderCredential(card, provider) {
6360
+ if (!provider.credential) {
6361
+ var note = document.createElement('div');
6362
+ note.className = 'cumulus-settings-credential-note';
6363
+ note.textContent = 'Authenticate with `claude login` on the gateway host.';
6364
+ card.appendChild(note);
6365
+ return;
6366
+ }
6367
+ var credentialRow = document.createElement('div');
6368
+ credentialRow.className = 'cumulus-settings-provider-credential';
6369
+ var credentialLabel = document.createElement('label');
6370
+ credentialLabel.textContent = 'API key';
6371
+ var credentialInput = document.createElement('input');
6372
+ credentialInput.type = 'password';
6373
+ credentialInput.autocomplete = 'off';
6374
+ credentialInput.className = 'cumulus-settings-input';
6375
+ credentialInput.placeholder = credentialClears[provider.id]
6376
+ ? 'Will be cleared on save'
6377
+ : '(unset)';
6378
+ credentialInput.value = credentialValues[provider.id] || '';
6379
+ credentialInput.setAttribute('data-testid', provider.credentialTestId);
6380
+ credentialInput.addEventListener('input', function () {
6381
+ credentialValues[provider.id] = credentialInput.value;
6382
+ credentialClears[provider.id] = false;
6383
+ credentialInput.placeholder = '(unset)';
6384
+ });
6385
+ credentialInputs[provider.id] = credentialInput;
6386
+ var clear = document.createElement('button');
6387
+ clear.type = 'button';
6388
+ clear.className = 'cumulus-settings-clear-btn';
6389
+ clear.textContent = 'Clear';
6390
+ clear.setAttribute('data-testid', 'webchat-gw-clear-' + provider.id + '-key');
6391
+ clear.addEventListener('click', function () {
6392
+ credentialClears[provider.id] = true;
6393
+ credentialValues[provider.id] = '';
6394
+ credentialInput.value = '';
6395
+ credentialInput.placeholder = 'Will be cleared on save';
6396
+ setSettingsStatus('Credential marked for removal', '');
6397
+ });
6398
+ credentialRow.appendChild(credentialLabel);
6399
+ credentialRow.appendChild(credentialInput);
6400
+ credentialRow.appendChild(clear);
6401
+ card.appendChild(credentialRow);
6402
+ }
6403
+
6404
+ function renderGwModels() {
6405
+ gwList.innerHTML = '';
6406
+ credentialInputs = {};
6407
+ providerDefs.forEach(function (provider) {
6408
+ var providerModels = gwState.catalog.filter(function (model) {
6409
+ return model.provider === provider.id;
6410
+ });
6411
+ var card = document.createElement('section');
6412
+ card.className = 'cumulus-settings-provider-card';
6413
+ card.setAttribute('data-testid', 'webchat-gw-provider-' + provider.id);
6414
+ var head = document.createElement('div');
6415
+ head.className = 'cumulus-settings-provider-head';
6416
+ var heading = document.createElement('div');
6417
+ var title = document.createElement('div');
6418
+ title.className = 'cumulus-settings-provider-title';
6419
+ title.textContent = provider.title;
6420
+ var description = document.createElement('div');
6421
+ description.className = 'cumulus-settings-provider-desc';
6422
+ description.textContent = provider.description;
6423
+ heading.appendChild(title);
6424
+ heading.appendChild(description);
6425
+ var count = document.createElement('span');
6426
+ count.className = 'cumulus-settings-provider-count';
6427
+ count.textContent =
6428
+ providerModels.length + (providerModels.length === 1 ? ' model' : ' models');
6429
+ head.appendChild(heading);
6430
+ head.appendChild(count);
6431
+ card.appendChild(head);
6432
+ var list = document.createElement('div');
6433
+ list.className = 'cumulus-settings-provider-models';
6434
+ if (!providerModels.length) {
6435
+ var empty = document.createElement('div');
6436
+ empty.className = 'cumulus-settings-provider-empty';
6437
+ empty.textContent = 'No ' + provider.title + ' models configured.';
6438
+ list.appendChild(empty);
6439
+ } else {
6440
+ providerModels.forEach(function (model, index) {
6441
+ list.appendChild(renderModelRow(model, provider, index));
6442
+ });
6443
+ }
6444
+ card.appendChild(list);
6445
+ // The Claude sub-model default only surfaces when it can actually
6446
+ // diverge from the gateway default (task 130) — gateway default on a
6447
+ // direct provider, ≥2 Anthropic models. Otherwise coupling makes it
6448
+ // redundant and it stays out of the way.
6449
+ if (provider.id === 'anthropic' && claudeSubDefaultVisible(gwState.catalog)) {
6450
+ var subRow = document.createElement('div');
6451
+ subRow.className = 'cumulus-settings-claude-default-row';
6452
+ var subLabel = document.createElement('label');
6453
+ subLabel.textContent = 'When a thread picks Claude, use';
6454
+ var subSelect = document.createElement('select');
6455
+ subSelect.className = 'cumulus-settings-select';
6456
+ subSelect.setAttribute('data-testid', 'webchat-gw-claude-default-select');
6457
+ var subDefault = '';
6458
+ providerModels.forEach(function (model) {
6459
+ var opt = document.createElement('option');
6460
+ opt.value = model.id;
6461
+ opt.textContent = model.label || model.id;
6462
+ subSelect.appendChild(opt);
6463
+ if (model.providerDefault) subDefault = model.id;
6464
+ });
6465
+ subSelect.value = subDefault || (providerModels[0] && providerModels[0].id) || '';
6466
+ subSelect.addEventListener('change', function () {
6467
+ applyClaudeSubDefault(gwState.catalog, subSelect.value);
6468
+ });
6469
+ subRow.appendChild(subLabel);
6470
+ subRow.appendChild(subSelect);
6471
+ card.appendChild(subRow);
6472
+ }
6473
+ renderCredential(card, provider);
6474
+ gwList.appendChild(card);
6475
+ });
6476
+ }
6477
+
6478
+ function renderDefaultSelect() {
6479
+ defaultSelect.innerHTML = '';
6480
+ if (!gwState.catalog.length) {
6481
+ var none = document.createElement('option');
6482
+ none.value = '';
6483
+ none.textContent = 'No models configured';
6484
+ defaultSelect.appendChild(none);
6485
+ defaultSelect.disabled = true;
6486
+ return;
6487
+ }
6488
+ defaultSelect.disabled = false;
6489
+ providerDefs.forEach(function (provider) {
6490
+ var group = null;
6491
+ gwState.catalog.forEach(function (model) {
6492
+ if (model.provider !== provider.id) return;
6493
+ if (!group) {
6494
+ group = document.createElement('optgroup');
6495
+ group.label = provider.title;
6496
+ defaultSelect.appendChild(group);
6497
+ }
6498
+ var opt = document.createElement('option');
6499
+ opt.value = model.id;
6500
+ opt.textContent = model.label || model.id;
6501
+ group.appendChild(opt);
6502
+ });
6503
+ });
6504
+ var current = gwState.catalog.find(function (model) {
6505
+ return model.gatewayDefault;
6506
+ });
6507
+ defaultSelect.value = current ? current.id : gwState.catalog[0].id;
6508
+ }
6509
+
6510
+ function renderAll() {
6511
+ renderDefaultSelect();
6512
+ renderGwModels();
6513
+ }
6514
+
6515
+ addProvider.addEventListener('change', function () {
6516
+ addContext.disabled = addProvider.value === 'anthropic';
6517
+ if (addContext.disabled) addContext.value = '';
6518
+ });
6519
+ addBtn.addEventListener('click', function () {
6520
+ var id = (addId.value || '').trim();
6521
+ var provider = addProvider.value;
6522
+ var context = addContext.value ? Number(addContext.value) : undefined;
6523
+ if (!id) {
6524
+ setSettingsStatus('Model ID is required', 'error');
6525
+ return;
6526
+ }
6527
+ if (
6528
+ id === 'claude' ||
6529
+ gwState.catalog.some(function (item) {
6530
+ return item.id === id;
6531
+ })
6532
+ ) {
6533
+ setSettingsStatus('Duplicate or reserved model ID: ' + id, 'error');
6534
+ return;
6535
+ }
6536
+ if (context !== undefined && (!Number.isInteger(context) || context <= 0)) {
6537
+ setSettingsStatus('Context window must be a positive whole number', 'error');
6538
+ return;
6539
+ }
6540
+ var model = {
6541
+ id: id,
6542
+ label: (addLabel.value || '').trim() || id,
6543
+ provider: provider,
6544
+ contextWindow: provider === 'anthropic' ? undefined : context,
6545
+ gatewayDefault: gwState.catalog.length === 0,
6546
+ providerDefault:
6547
+ provider === 'anthropic' &&
6548
+ !gwState.catalog.some(function (item) {
6549
+ return item.provider === 'anthropic';
6550
+ }),
6551
+ };
6552
+ gwState.catalog.push(model);
6553
+ addId.value = '';
6554
+ addLabel.value = '';
6555
+ addContext.value = '';
6556
+ setSettingsStatus('', '');
6557
+ renderAll();
6558
+ });
6559
+
6560
+ function refreshOpenModelCatalogs() {
6561
+ var refresh = new XMLHttpRequest();
6562
+ refresh.open('GET', '/api/models');
6563
+ refresh.setRequestHeader('X-API-Key', activeApiKey);
6564
+ refresh.onreadystatechange = function () {
6565
+ if (refresh.readyState !== 4 || refresh.status !== 200) return;
6566
+ try {
6567
+ window.dispatchEvent(
6568
+ new CustomEvent('cumulus-model-catalog-updated', {
6569
+ detail: JSON.parse(refresh.responseText),
6570
+ })
6571
+ );
6572
+ } catch (e) {
6573
+ /* keep the saved config even if a panel refresh fails */
6574
+ }
6575
+ };
6576
+ refresh.send();
6577
+ }
6578
+
6579
+ saveBtn.addEventListener('click', function () {
6580
+ var validationError = validateModelCatalog(gwState.catalog);
6581
+ if (validationError) {
6582
+ setSettingsStatus(validationError, 'error');
6583
+ return;
6584
+ }
6585
+ saveBtn.disabled = true;
6586
+ setSettingsStatus('Saving…', '');
6587
+ var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), {
6588
+ openai: {
6589
+ value: credentialValues.openai,
6590
+ clear: credentialClears.openai,
6591
+ },
6592
+ huggingface: {
6593
+ value: credentialValues.huggingface,
6594
+ clear: credentialClears.huggingface,
6595
+ },
6596
+ license: {
6597
+ value: credentialValues.license,
6598
+ clear: credentialClears.license,
6599
+ },
6600
+ });
6601
+ var save = new XMLHttpRequest();
6602
+ save.open('PUT', '/api/config');
6603
+ save.setRequestHeader('Content-Type', 'application/json');
6604
+ save.setRequestHeader('X-API-Key', activeApiKey);
6605
+ save.onreadystatechange = function () {
6606
+ if (save.readyState !== 4) return;
6607
+ saveBtn.disabled = false;
6608
+ if (save.status === 200) {
6609
+ try {
6610
+ var response = JSON.parse(save.responseText);
6611
+ gwState = gatewayConfigToModelCatalog(response);
6612
+ credentialClears.openai = false;
6613
+ credentialClears.huggingface = false;
6614
+ credentialClears.license = false;
6615
+ credentialValues.openai = response.openaiApiKey || '';
6616
+ credentialValues.huggingface = response.hfApiKey || '';
6617
+ credentialValues.license = response.licenseKey || '';
6618
+ gwLicenseMasked = response.licenseKey || '';
6619
+ gwLicenseInfo = response.license || null;
6620
+ renderAll();
6621
+ renderLicenseState(gwLicenseMasked, gwLicenseInfo);
6622
+ setSettingsStatus(
6623
+ gwLicenseInfo && gwLicenseInfo.pendingReload
6624
+ ? 'Saved — license takes effect on gateway reload'
6625
+ : 'Saved — applies to new turns immediately',
6626
+ 'success'
6627
+ );
6628
+ refreshOpenModelCatalogs();
6629
+ } catch (e) {
6630
+ setSettingsStatus('Saved, but the response could not be rendered', 'error');
6631
+ }
6632
+ } else {
6633
+ setSettingsStatus('Error ' + save.status, 'error');
6634
+ }
6635
+ };
6636
+ save.send(JSON.stringify(payload));
6637
+ });
6638
+
6639
+ (function loadGatewayConfig() {
6640
+ var load = new XMLHttpRequest();
6641
+ load.open('GET', '/api/config');
6642
+ load.setRequestHeader('X-API-Key', activeApiKey);
6643
+ load.onreadystatechange = function () {
6644
+ if (load.readyState !== 4) return;
6645
+ if (load.status === 200) {
6646
+ try {
6647
+ var response = JSON.parse(load.responseText);
6648
+ gwState = gatewayConfigToModelCatalog(response);
6649
+ credentialValues.openai = response.openaiApiKey || '';
6650
+ credentialValues.huggingface = response.hfApiKey || '';
6651
+ credentialValues.license = response.licenseKey || '';
6652
+ gwLicenseMasked = response.licenseKey || '';
6653
+ gwLicenseInfo = response.license || null;
6654
+ renderAll();
6655
+ renderLicenseState(gwLicenseMasked, gwLicenseInfo);
6656
+ } catch (e) {
6657
+ setSettingsStatus('Gateway settings response was invalid', 'error');
6658
+ }
6659
+ } else {
6660
+ setSettingsStatus('Load failed (' + load.status + ')', 'error');
6661
+ }
6662
+ };
6663
+ load.send();
6664
+ })();
6665
+ }
6666
+
5849
6667
  function buildThreadPanel(threadName) {
5850
6668
  var state = getThreadState(threadName);
5851
6669
 
@@ -5907,14 +6725,14 @@
5907
6725
  claudeModelSelect.setAttribute('data-testid', 'webchat-claude-model-select');
5908
6726
  claudeModelSelect.setAttribute('data-thread-claude-model', threadName);
5909
6727
  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
- ];
6728
+ // Populated ONLY from /api/models (resp.claudeModels)task 129 deleted
6729
+ // the hardcoded fallback list. An empty catalog hides the select entirely
6730
+ // (visibility is re-derived in updateEffortVisibility, which also gates
6731
+ // on model === 'claude'); the Settings gear is where models get added.
6732
+ var claudeModelListEmpty = true;
5916
6733
  function populateClaudeModels(list) {
5917
- var models = list && list.length ? list : CLAUDE_MODELS_FALLBACK;
6734
+ var models = list && list.length ? list : [];
6735
+ claudeModelListEmpty = models.length === 0;
5918
6736
  claudeModelSelect.innerHTML = '';
5919
6737
  var def = null;
5920
6738
  models.forEach(function (m) {
@@ -5925,6 +6743,7 @@
5925
6743
  if (m.default) def = m.id;
5926
6744
  });
5927
6745
  claudeModelSelect.value = def || (models[0] && models[0].id) || '';
6746
+ claudeModelSelect.style.display = claudeModelListEmpty ? 'none' : '';
5928
6747
  }
5929
6748
  populateClaudeModels(null);
5930
6749
 
@@ -6024,7 +6843,7 @@
6024
6843
  function updateEffortVisibility() {
6025
6844
  var isClaude = modelSelect.value === 'claude';
6026
6845
  effortSelect.style.display = isClaude ? '' : 'none';
6027
- claudeModelSelect.style.display = isClaude ? '' : 'none';
6846
+ claudeModelSelect.style.display = isClaude && !claudeModelListEmpty ? '' : 'none';
6028
6847
  // Hide the whole Model/Effort popover rows (label included) for non-Claude providers.
6029
6848
  var mr = panel.querySelector('[data-config-row="model"]');
6030
6849
  var er = panel.querySelector('[data-config-row="effort"]');
@@ -6185,546 +7004,13 @@
6185
7004
  modelPicker.appendChild(configBreadcrumb);
6186
7005
  modelPicker.appendChild(modelPopover);
6187
7006
 
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
7007
  panelHeader.appendChild(modelPicker);
6715
7008
 
6716
- // Action buttons (settings + include + revert)
7009
+ // Action buttons (include + revert + select) — gateway settings moved
7010
+ // to the topbar gear (task 128)
6717
7011
  var actions = document.createElement('div');
6718
7012
  actions.className = 'cumulus-panel-actions';
6719
7013
 
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
7014
  var includeBtn = document.createElement('button');
6729
7015
  includeBtn.className = 'cumulus-panel-action-btn';
6730
7016
  includeBtn.setAttribute('data-testid', 'webchat-include-btn');