pilotswarm 0.5.41 → 0.5.43

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 (112) hide show
  1. package/mcp/README.md +25 -1
  2. package/mcp/dist/src/auth.d.ts.map +1 -1
  3. package/mcp/dist/src/auth.js +5 -3
  4. package/mcp/dist/src/auth.js.map +1 -1
  5. package/mcp/dist/src/resources/models.d.ts.map +1 -1
  6. package/mcp/dist/src/resources/models.js +21 -14
  7. package/mcp/dist/src/resources/models.js.map +1 -1
  8. package/mcp/dist/src/server.d.ts.map +1 -1
  9. package/mcp/dist/src/server.js +2 -0
  10. package/mcp/dist/src/server.js.map +1 -1
  11. package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -1
  12. package/mcp/dist/src/tools/agent-packages.js +28 -16
  13. package/mcp/dist/src/tools/agent-packages.js.map +1 -1
  14. package/mcp/dist/src/tools/models.d.ts.map +1 -1
  15. package/mcp/dist/src/tools/models.js +55 -47
  16. package/mcp/dist/src/tools/models.js.map +1 -1
  17. package/mcp/dist/src/tools/providers.d.ts +18 -0
  18. package/mcp/dist/src/tools/providers.d.ts.map +1 -0
  19. package/mcp/dist/src/tools/providers.js +284 -0
  20. package/mcp/dist/src/tools/providers.js.map +1 -0
  21. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  22. package/mcp/dist/src/tools/sessions.js +10 -1
  23. package/mcp/dist/src/tools/sessions.js.map +1 -1
  24. package/mcp/dist/src/tools/system.js +2 -2
  25. package/mcp/dist/src/tools/system.js.map +1 -1
  26. package/mcp/dist/src/util/respond.d.ts.map +1 -1
  27. package/mcp/dist/src/util/respond.js +6 -0
  28. package/mcp/dist/src/util/respond.js.map +1 -1
  29. package/package.json +3 -3
  30. package/tui/README.md +8 -5
  31. package/tui/src/agents-cli.js +80 -78
  32. package/tui/src/app.js +119 -17
  33. package/tui/src/node-sdk-transport.js +150 -347
  34. package/ui/core/src/controller.js +1079 -23
  35. package/ui/core/src/history.js +25 -1
  36. package/ui/core/src/reducer.js +649 -1
  37. package/ui/core/src/selectors.js +906 -6
  38. package/ui/core/src/state.js +163 -3
  39. package/ui/react/src/components.js +146 -4
  40. package/ui/react/src/web-app.js +1942 -125
  41. package/web/README.md +4 -4
  42. package/web/api/router.js +33 -0
  43. package/web/dist/assets/{abnfDiagram-VRR7QNED-kpxu_PEa.js → abnfDiagram-VRR7QNED-BqKIaTz5.js} +1 -1
  44. package/web/dist/assets/{arc-BnVQmtB5.js → arc-DCSwKNAt.js} +1 -1
  45. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CDynQ_6_.js → architectureDiagram-ZJ3FMSHR-B6j7Bv5B.js} +1 -1
  46. package/web/dist/assets/{blockDiagram-677ZJIJ3-BUpYaSDb.js → blockDiagram-677ZJIJ3-BT1iegLO.js} +1 -1
  47. package/web/dist/assets/{c4Diagram-LMCZKHZV-ZL8eskDB.js → c4Diagram-LMCZKHZV-DXEt-_ny.js} +1 -1
  48. package/web/dist/assets/channel-BLVoqfUa.js +1 -0
  49. package/web/dist/assets/{chunk-2Q5K7J3B-o0Nsyp1T.js → chunk-2Q5K7J3B-B144-8I2.js} +1 -1
  50. package/web/dist/assets/{chunk-32BRIVSS-CH5CSNRP.js → chunk-32BRIVSS-CMHrtYOC.js} +1 -1
  51. package/web/dist/assets/{chunk-5VM5RSS4-C9wwtPWL.js → chunk-5VM5RSS4-4r8OmbGE.js} +1 -1
  52. package/web/dist/assets/{chunk-EX3LRPZG--F9yFbhA.js → chunk-EX3LRPZG-R_j3wosN.js} +1 -1
  53. package/web/dist/assets/{chunk-JWPE2WC7-BPIstyWL.js → chunk-JWPE2WC7-BKlFEYXC.js} +1 -1
  54. package/web/dist/assets/{chunk-MOJQB5TN-5Dm2u-0k.js → chunk-MOJQB5TN-C2S4TPSa.js} +1 -1
  55. package/web/dist/assets/{chunk-RYQCIY6F-Dbtf9iQK.js → chunk-RYQCIY6F-Y2kpzPD7.js} +1 -1
  56. package/web/dist/assets/{chunk-V7JOEXUC-DrZEBJ5l.js → chunk-V7JOEXUC-kAOP_3Kc.js} +1 -1
  57. package/web/dist/assets/{chunk-VR4S4FIN-AqAUynqK.js → chunk-VR4S4FIN-B2Au1EfR.js} +1 -1
  58. package/web/dist/assets/{chunk-XXDRQBXY-DQlFaqCy.js → chunk-XXDRQBXY-CZfTG6DD.js} +1 -1
  59. package/web/dist/assets/classDiagram-OUVF2IWQ-BgaYclQI.js +1 -0
  60. package/web/dist/assets/classDiagram-v2-EOCWNBFH-BgaYclQI.js +1 -0
  61. package/web/dist/assets/{cose-bilkent-JH36ORCC-RAtPG1Ou.js → cose-bilkent-JH36ORCC--tNLpMEb.js} +1 -1
  62. package/web/dist/assets/{cynefin-VYW2F7L2-CbeHaUWZ.js → cynefin-VYW2F7L2-BeIRFyvX.js} +1 -1
  63. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-B6paqF5Y.js → cynefinDiagram-TSTJHNR4-Bm5lb_xz.js} +1 -1
  64. package/web/dist/assets/{dagre-VKFMJZFB-DziTBO2K.js → dagre-VKFMJZFB-Cz1i_B0y.js} +1 -1
  65. package/web/dist/assets/{diagram-FQU43EPY-CPRUiLcr.js → diagram-FQU43EPY-BF0j7c1T.js} +1 -1
  66. package/web/dist/assets/{diagram-G47NLZAW-CIMVdcL1.js → diagram-G47NLZAW-C2QWJ-Y4.js} +1 -1
  67. package/web/dist/assets/{diagram-NH7WQ7WH-CHPFllxR.js → diagram-NH7WQ7WH-BiNeThAA.js} +1 -1
  68. package/web/dist/assets/{diagram-OA4YK3LP-DwS6unBJ.js → diagram-OA4YK3LP-Cq7_37rx.js} +1 -1
  69. package/web/dist/assets/{diagram-WEI45ONY-DKWgkObI.js → diagram-WEI45ONY-Cl0GjIma.js} +1 -1
  70. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-zAVWV6-D.js → ebnfDiagram-CCIWWBDH-C-dTpVrf.js} +1 -1
  71. package/web/dist/assets/{erDiagram-Q63AITRT-D0tYX4h0.js → erDiagram-Q63AITRT-RPSg6Lws.js} +1 -1
  72. package/web/dist/assets/{flowDiagram-23GEKE2U-Cfq4I7R0.js → flowDiagram-23GEKE2U-CslnZm0D.js} +1 -1
  73. package/web/dist/assets/{ganttDiagram-NO4QXBWP-wgBAHr5I.js → ganttDiagram-NO4QXBWP-dzr6nimS.js} +1 -1
  74. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BTIPX5PR.js → gitGraphDiagram-IHSO6WYX-UpjDHGP0.js} +1 -1
  75. package/web/dist/assets/index-C-ujPKpy.css +1 -0
  76. package/web/dist/assets/index-Cf_e477H.js +24 -0
  77. package/web/dist/assets/{infoDiagram-FWYZ7A6U-CRLiQ87D.js → infoDiagram-FWYZ7A6U-R4qLdwe7.js} +1 -1
  78. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CwzwUis1.js → ishikawaDiagram-FXEZZL3T-Cst7PjW4.js} +1 -1
  79. package/web/dist/assets/{journeyDiagram-5HDEW3XC-B9pqMeFw.js → journeyDiagram-5HDEW3XC-CugDTBMp.js} +1 -1
  80. package/web/dist/assets/{kanban-definition-HUTT4EX6-CZ5Xky4p.js → kanban-definition-HUTT4EX6-D6Yf2Lgg.js} +1 -1
  81. package/web/dist/assets/{linear-BLZ2eH4a.js → linear-hUntimCu.js} +1 -1
  82. package/web/dist/assets/{mermaid.core-CPN1IVjz.js → mermaid.core-BhKlcGBI.js} +4 -4
  83. package/web/dist/assets/{mindmap-definition-LN4V7U3C-MdC8d2tc.js → mindmap-definition-LN4V7U3C-DKVBRUj4.js} +1 -1
  84. package/web/dist/assets/{papaparse.min-CtrVJupf.js → papaparse.min-DbTWBfDP.js} +1 -1
  85. package/web/dist/assets/{pegDiagram-2B236MQR-B2DqAc-A.js → pegDiagram-2B236MQR-DeilQyRr.js} +1 -1
  86. package/web/dist/assets/{pieDiagram-ENE6RG2P-C3QAoXZh.js → pieDiagram-ENE6RG2P-CSVE3nUJ.js} +1 -1
  87. package/web/dist/assets/pilotswarm-DaQpAPI0.js +97 -0
  88. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BoUr5qFH.js → quadrantDiagram-ABIIQ3AL-Di-WUa_j.js} +1 -1
  89. package/web/dist/assets/{railroadDiagram-RFXS5EU6-bZzE4KFN.js → railroadDiagram-RFXS5EU6-jBlJSP-m.js} +1 -1
  90. package/web/dist/assets/{requirementDiagram-TGXJPOKE-B2ENAqLS.js → requirementDiagram-TGXJPOKE-BlLuScOr.js} +1 -1
  91. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-AJZ3h46Z.js → sankeyDiagram-HTMAVEWB-D76NurlB.js} +1 -1
  92. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-QUf0VVE7.js → sequenceDiagram-DBY2YBRQ-BGPIUJo0.js} +1 -1
  93. package/web/dist/assets/{sizeCapture-X5ZJPWSS-Cv9RcUy6.js → sizeCapture-X5ZJPWSS-DKmGUrSI.js} +1 -1
  94. package/web/dist/assets/{stateDiagram-2N3HPSRC-hDiye45t.js → stateDiagram-2N3HPSRC-DAYHO_nW.js} +1 -1
  95. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-jZoHf_Ku.js +1 -0
  96. package/web/dist/assets/{swimlanes-5IMT3BWC-hSk_OF85.js → swimlanes-5IMT3BWC-CiRowk4M.js} +2 -2
  97. package/web/dist/assets/swimlanesDiagram-G3AALYLV-C0ms7UKB.js +8 -0
  98. package/web/dist/assets/{timeline-definition-FHXFAJF6-ccOZEJ5e.js → timeline-definition-FHXFAJF6-iN7kQL24.js} +1 -1
  99. package/web/dist/assets/{vennDiagram-L72KCM5P-Bk5DLH_V.js → vennDiagram-L72KCM5P-C6TtJ36b.js} +1 -1
  100. package/web/dist/assets/{wardleyDiagram-EHGQE667-C20AX8ot.js → wardleyDiagram-EHGQE667-CBiv7EU-.js} +1 -1
  101. package/web/dist/assets/{xychartDiagram-FW5EYKEG-DaHqmi-R.js → xychartDiagram-FW5EYKEG-zXUejHih.js} +1 -1
  102. package/web/dist/index.html +3 -3
  103. package/web/runtime.js +220 -2
  104. package/web/server.js +35 -7
  105. package/web/dist/assets/channel-Bi5A6sHR.js +0 -1
  106. package/web/dist/assets/classDiagram-OUVF2IWQ-D8dCxI54.js +0 -1
  107. package/web/dist/assets/classDiagram-v2-EOCWNBFH-D8dCxI54.js +0 -1
  108. package/web/dist/assets/index-OHj-gGHp.js +0 -24
  109. package/web/dist/assets/index-eTH1FBtN.css +0 -1
  110. package/web/dist/assets/pilotswarm-CF1nmpMy.js +0 -97
  111. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Bu4ufvaU.js +0 -1
  112. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTFePb1Y.js +0 -8
@@ -1,5 +1,5 @@
1
1
  import { UI_COMMANDS, FOCUS_REGIONS, INSPECTOR_TABS, cycleValue } from "./commands.js";
2
- import { canvasKey as canvasPrefKey } from "./state.js";
2
+ import { BUDGET_SERIES_DAYS, BUDGET_SERIES_RANGES, canvasKey as canvasPrefKey } from "./state.js";
3
3
  import { parseAgentSourceLink } from "./repo-links.js";
4
4
  import { importPackageFilesFromLink, readImportedPackageName } from "./repo-import.js";
5
5
  import {
@@ -44,6 +44,7 @@ import {
44
44
  selectFilesView,
45
45
  selectInspector,
46
46
  selectOutboxOverlayLines,
47
+ selectAdminConsole,
47
48
  selectSessionRows,
48
49
  selectSelectedFileBrowserItem,
49
50
  selectVisibleSessionRows,
@@ -211,6 +212,19 @@ function groupModelsByProvider(models = []) {
211
212
  return groups;
212
213
  }
213
214
 
215
+ /**
216
+ * Where the model picker lands.
217
+ *
218
+ * Never on a model that cannot run: when the preferred one (usually the
219
+ * default) is blocked, land on the first usable model instead, so the very
220
+ * first Enter does something.
221
+ */
222
+ function pickerSelectedIndex(items, preferredModel) {
223
+ let index = items.findIndex((item) => item.qualifiedName === preferredModel && !item.disabled);
224
+ if (index < 0) index = items.findIndex((item) => !item.disabled);
225
+ return index < 0 ? 0 : index;
226
+ }
227
+
214
228
  function normalizeReasoningEfforts(values) {
215
229
  if (!Array.isArray(values)) return [];
216
230
  const out = [];
@@ -1370,6 +1384,93 @@ const SESSION_NAV_SETTLE_MS = 140;
1370
1384
  // still expands freely while you are in the session; re-entering starts bounded.
1371
1385
  const HISTORY_REENTRY_MAX_EVENTS = 1_500;
1372
1386
 
1387
+ // ── Provider budgets ────────────────────────────────────────────────
1388
+ //
1389
+ // The model: docs/proposals/providers-and-budgets.md. A provider is a name
1390
+ // with a credential and a budget; a session runs `provider:model` and that
1391
+ // provider is charged. There are no pools, no payers and no access lists —
1392
+ // what a person may spend is decided by QUANTITY (limits, and an allowance
1393
+ // that divides each limit), never by identity.
1394
+ //
1395
+ // The surface reads this out of `state.budget` through
1396
+ // `selectProviderTable`. The one helper here is for the model PICKER, which
1397
+ // cannot: it reads the namespace fresh when it opens, because a provider
1398
+ // created a minute ago has to be offerable without a page load.
1399
+
1400
+ const PICKER_PERIOD_TITLE = { day: "Daily", week: "Weekly", month: "Monthly" };
1401
+
1402
+ /**
1403
+ * The message the server wrote, unchanged.
1404
+ *
1405
+ * A provider refusal is written for the person who hit it and names the
1406
+ * remedy — "the cluster default must be a shared provider", "there is no
1407
+ * provider named carol-ghcp". Replacing it with a generic sentence throws
1408
+ * away the only part of the answer anyone can act on. Only a failure with
1409
+ * nothing to say at all gets a fallback.
1410
+ */
1411
+ function budgetRefusalMessage(error) {
1412
+ const message = typeof error?.message === "string" ? error.message.trim() : "";
1413
+ if (message) return message;
1414
+ const raw = String(error ?? "").trim();
1415
+ return raw || "The request failed.";
1416
+ }
1417
+
1418
+ /**
1419
+ * What the model picker prints beside a provider, so the choice is made
1420
+ * knowing. The state names are the ones `selectProviderTable` uses, so a
1421
+ * component can style both from one set of classes.
1422
+ *
1423
+ * A provider with no STATUS row gets "unknown", never a confident "no
1424
+ * limits": one is a claim about the budget and the other is an admission
1425
+ * that it was not read, and only one of them is safe to act on.
1426
+ */
1427
+ function providerPickerNote(provider, statusRow, now = Date.now()) {
1428
+ if (provider?.hasCredential === false) {
1429
+ return { state: "no_credential", text: "no credential — nothing can run on it" };
1430
+ }
1431
+ const held = provider?.holdIndefinite === true
1432
+ || (provider?.holdUntilUtc ? Date.parse(provider.holdUntilUtc) > now : false);
1433
+ if (held) {
1434
+ return { state: "hold", text: "on hold — new turns wait until it is released" };
1435
+ }
1436
+ if (!statusRow) return { state: "unknown", text: "budget unknown" };
1437
+ const rules = Array.isArray(statusRow.rules) ? statusRow.rules : [];
1438
+ if (rules.length === 0) return { state: "no_limits", text: "no limit" };
1439
+ // The binding one: what is already stopping turns, else what is nearest
1440
+ // to stopping them. Ranking by percentage alone put a comfortable weekly
1441
+ // limit above a daily one that was already full.
1442
+ const scored = rules.map((rule) => {
1443
+ const limitTokens = Number(rule?.limitTokens) || 0;
1444
+ const usedTokens = Number(rule?.usedTokens) || 0;
1445
+ const ceilingTokens = rule?.ceilingTokens == null ? null : Number(rule.ceilingTokens);
1446
+ const yourUsedTokens = Number(rule?.yourUsedTokens) || 0;
1447
+ return {
1448
+ periodLabel: PICKER_PERIOD_TITLE[rule?.period] || String(rule?.period || ""),
1449
+ pct: limitTokens > 0 ? Math.floor((usedTokens / limitTokens) * 100) : 0,
1450
+ atLimit: limitTokens > 0 && usedTokens >= limitTokens,
1451
+ atCeiling: ceilingTokens != null && ceilingTokens > 0 && yourUsedTokens >= ceilingTokens,
1452
+ };
1453
+ });
1454
+ const worst = [...scored].sort((a, b) => (
1455
+ ((a.atLimit || a.atCeiling) ? 0 : 1) - ((b.atLimit || b.atCeiling) ? 0 : 1) || b.pct - a.pct
1456
+ ))[0];
1457
+ if (worst.atLimit) {
1458
+ return {
1459
+ state: "at_limit",
1460
+ text: `at its ${worst.periodLabel.toLowerCase()} limit — a session here waits for the reset`,
1461
+ };
1462
+ }
1463
+ // The provider still has room; this person does not. Different remedy,
1464
+ // so different words: raising the limit changes nothing here.
1465
+ if (worst.atCeiling) {
1466
+ return {
1467
+ state: "at_limit",
1468
+ text: `your share of its ${worst.periodLabel.toLowerCase()} limit is used up`,
1469
+ };
1470
+ }
1471
+ return { state: "under_limit", text: `${worst.pct}% of its ${worst.periodLabel.toLowerCase()} limit used` };
1472
+ }
1473
+
1373
1474
  export class PilotSwarmUiController {
1374
1475
  constructor({ store, transport }) {
1375
1476
  this.store = store;
@@ -2443,6 +2544,10 @@ export class PilotSwarmUiController {
2443
2544
  }
2444
2545
  if (canvasSeed.length) this.dispatch({ type: "canvas/seed", entries: canvasSeed });
2445
2546
  this.dispatch({ type: "sessions/loaded", sessions });
2547
+ // A row that says only "waiting" says nothing anyone can act on. Which
2548
+ // limit, whose allowance, which hold, which missing name — that lives
2549
+ // in the provider reads, so take them while the list holds a wait.
2550
+ this.refreshBudgetPausesIfStale(sessions).catch(() => {});
2446
2551
  this.refreshOpenSessionOwnerFilterModal();
2447
2552
  const selected = this.getState().sessions.activeSessionId;
2448
2553
  const syncedIds = new Set();
@@ -2940,6 +3045,7 @@ export class PilotSwarmUiController {
2940
3045
  async openAdminConsole() {
2941
3046
  this.dispatch({ type: "admin/visibility", visible: true });
2942
3047
  await this.refreshAdminProfile().catch(() => {});
3048
+ void this.refreshAdminModelProviders().catch(() => {});
2943
3049
  void this.refreshAdminAgentPackages().catch(() => {});
2944
3050
  }
2945
3051
 
@@ -3010,10 +3116,328 @@ export class PilotSwarmUiController {
3010
3116
  this.dispatch({ type: "admin/ghcpKey/setSystemTarget", value: Boolean(value) });
3011
3117
  }
3012
3118
 
3119
+ async refreshAdminModelProviders() {
3120
+ const required = ["listProviders", "getModelDefaults"];
3121
+ if (required.some((name) => typeof this.transport[name] !== "function")) {
3122
+ this.dispatch({ type: "admin/modelProviders/loadFailed", error: "Model provider management is not available on this transport." });
3123
+ return null;
3124
+ }
3125
+ this.dispatch({ type: "admin/modelProviders/loading" });
3126
+ try {
3127
+ const [providerResult, modelResult, defaults] = await Promise.all([
3128
+ this.transport.listProviders(),
3129
+ typeof this.transport.getModelsByProvider === "function"
3130
+ ? this.transport.getModelsByProvider()
3131
+ : this.transport.listModels(),
3132
+ this.transport.getModelDefaults(),
3133
+ ]);
3134
+ const models = Array.isArray(modelResult)
3135
+ && modelResult.some((entry) => Array.isArray(entry?.models))
3136
+ ? modelResult.flatMap((group) => (group.models || []).map((model) => ({
3137
+ ...model,
3138
+ providerId: group.providerId,
3139
+ providerType: group.type || model.providerType,
3140
+ })))
3141
+ : modelResult;
3142
+ const providers = Array.isArray(providerResult)
3143
+ ? providerResult
3144
+ : (Array.isArray(providerResult?.providers) ? providerResult.providers : []);
3145
+ this.dispatch({
3146
+ type: "admin/modelProviders/loaded",
3147
+ providers,
3148
+ models: Array.isArray(models) ? models : [],
3149
+ defaults,
3150
+ });
3151
+ return { providers, models, defaults };
3152
+ } catch (error) {
3153
+ this.dispatch({ type: "admin/modelProviders/loadFailed", error: error?.message || String(error) });
3154
+ return null;
3155
+ }
3156
+ }
3157
+
3158
+ async _runAdminModelProviderMutation(pending, run) {
3159
+ this.dispatch({ type: "admin/modelProviders/mutationPending", pending });
3160
+ try {
3161
+ const result = await run();
3162
+ this.dispatch({ type: "admin/modelProviders/mutationDone" });
3163
+ await this.refreshAdminModelProviders();
3164
+ return { ok: true, result: result ?? null, error: null };
3165
+ } catch (error) {
3166
+ const message = error?.message || String(error);
3167
+ this.dispatch({ type: "admin/modelProviders/mutationFailed", error: message });
3168
+ return { ok: false, result: null, error: message };
3169
+ }
3170
+ }
3171
+
3172
+ async createAdminProvider({ name, type, credentials, baseUrl = null, shared = false } = {}) {
3173
+ const op = shared ? "createProvider" : "createMyProvider";
3174
+ if (typeof this.transport[op] !== "function") {
3175
+ this.dispatch({ type: "admin/modelProviders/mutationFailed", error: "Provider creation is not available on this transport." });
3176
+ return { ok: false, result: null, error: "Provider creation is not available on this transport." };
3177
+ }
3178
+ return this._runAdminModelProviderMutation("create", () => this.transport[op]({
3179
+ name, type, credentials, baseUrl,
3180
+ }));
3181
+ }
3182
+
3183
+ async deleteAdminProvider(name, { shared = false } = {}) {
3184
+ const op = shared ? "deleteProvider" : "deleteMyProvider";
3185
+ if (typeof this.transport[op] !== "function") {
3186
+ return { ok: false, result: null, error: "Provider deletion is not available on this transport." };
3187
+ }
3188
+ return this._runAdminModelProviderMutation("delete", () => this.transport[op](name));
3189
+ }
3190
+
3191
+ async setAdminProviderSystemUse(provider, enabled) {
3192
+ if (typeof this.transport.setProviderSystemUse !== "function") {
3193
+ return { ok: false, result: null, error: "System-use controls are not available on this transport." };
3194
+ }
3195
+ return this._runAdminModelProviderMutation("systemUse", () => this.transport.setProviderSystemUse({
3196
+ provider, enabled: enabled === true,
3197
+ }));
3198
+ }
3199
+
3200
+ async setAdminModelDefault(scope, choice = null) {
3201
+ if (typeof this.transport.setModelDefault !== "function") {
3202
+ return { ok: false, result: null, error: "Model defaults are not available on this transport." };
3203
+ }
3204
+ return this._runAdminModelProviderMutation(`${scope}Default`, () => this.transport.setModelDefault({
3205
+ scope,
3206
+ provider: choice?.provider ?? null,
3207
+ model: choice?.qualifiedName ?? choice?.model ?? null,
3208
+ reasoningEffort: choice?.reasoningEffort ?? choice?.defaultReasoningEffort ?? null,
3209
+ contextTier: choice?.contextTier ?? choice?.defaultContextTier ?? null,
3210
+ }));
3211
+ }
3212
+
3213
+ async setAdminSystemModelDefault(choice = null, { restartDisposition = null } = {}) {
3214
+ if (typeof this.transport.setSystemModelDefault !== "function") {
3215
+ return { ok: false, result: null, error: "The system model default is not available on this transport." };
3216
+ }
3217
+ return this._runAdminModelProviderMutation("systemDefault", () => this.transport.setSystemModelDefault({
3218
+ provider: choice?.provider ?? null,
3219
+ model: choice?.qualifiedName ?? choice?.model ?? null,
3220
+ reasoningEffort: choice?.reasoningEffort ?? choice?.defaultReasoningEffort ?? null,
3221
+ contextTier: choice?.contextTier ?? choice?.defaultContextTier ?? null,
3222
+ restartExisting: restartDisposition ? { disposition: restartDisposition } : false,
3223
+ }));
3224
+ }
3225
+
3226
+ async setAdminSystemAgentModel(agentId, choice) {
3227
+ if (typeof this.transport.setSystemSessionModel !== "function") {
3228
+ return { ok: false, result: null, error: "System-agent overrides are not available on this transport." };
3229
+ }
3230
+ return this._runAdminModelProviderMutation("systemOverride", () => this.transport.setSystemSessionModel({
3231
+ agentId,
3232
+ provider: choice?.provider,
3233
+ model: choice?.qualifiedName ?? choice?.model,
3234
+ reasoningEffort: choice?.reasoningEffort ?? choice?.defaultReasoningEffort ?? null,
3235
+ contextTier: choice?.contextTier ?? choice?.defaultContextTier ?? null,
3236
+ }));
3237
+ }
3238
+
3239
+ async clearAdminSystemAgentModel(agentId) {
3240
+ if (typeof this.transport.clearSystemSessionModel !== "function") {
3241
+ return { ok: false, result: null, error: "System-agent overrides are not available on this transport." };
3242
+ }
3243
+ return this._runAdminModelProviderMutation("systemOverride", () => this.transport.clearSystemSessionModel(agentId));
3244
+ }
3245
+
3246
+ beginAdminCreateProvider({ shared = false } = {}) {
3247
+ const providers = this.getState().admin?.modelProviders || {};
3248
+ const typeId = (providers.models || []).find((model) => /github/i.test(model?.providerId || ""))?.providerId
3249
+ || providers.models?.[0]?.providerId
3250
+ || "github-copilot";
3251
+ const taken = new Set((providers.providers || []).map((provider) => String(provider?.name || "").toLowerCase()));
3252
+ const subject = String(this.getState().admin?.profile?.subject || "").replace(/[^A-Za-z0-9]/gu, "").slice(-10).toLowerCase();
3253
+ const base = subject ? `ghcp-${subject}` : "my-ghcp";
3254
+ let name = base;
3255
+ let suffix = 2;
3256
+ while (taken.has(name.toLowerCase())) {
3257
+ name = `${base}-${suffix}`;
3258
+ suffix += 1;
3259
+ }
3260
+ this.dispatch({ type: "admin/modelProviders/createBegin", name, typeId, shared });
3261
+ }
3262
+
3263
+ beginAdminCreateGithubProvider() {
3264
+ this.beginAdminCreateProvider({ shared: false });
3265
+ }
3266
+
3267
+ cycleAdminProviderCreateType() {
3268
+ const providers = selectAdminConsole(this.getState()).modelProviders;
3269
+ const create = this.getState().admin?.modelProviders?.create;
3270
+ if (!create?.editing || create.saving || create.stage !== "name") return;
3271
+ const types = providers.providerTypes || [];
3272
+ if (!types.length) return;
3273
+ const index = Math.max(0, types.findIndex((type) => type.id === create.typeId));
3274
+ this.dispatch({ type: "admin/modelProviders/createType", typeId: types[(index + 1) % types.length].id });
3275
+ }
3276
+
3277
+ setAdminProviderCreateDraft(draft, cursorIndex = String(draft || "").length) {
3278
+ this.dispatch({ type: "admin/modelProviders/createDraft", draft, cursorIndex });
3279
+ }
3280
+
3281
+ insertAdminProviderCreateText(text) {
3282
+ const create = this.getState().admin?.modelProviders?.create;
3283
+ if (!create?.editing || create.saving) return;
3284
+ const sanitized = String(text || "").replace(/\r?\n/gu, "");
3285
+ const next = insertPromptTextAtCursor(create.draft || "", create.cursorIndex || 0, sanitized);
3286
+ this.setAdminProviderCreateDraft(next.prompt, next.cursor);
3287
+ }
3288
+
3289
+ deleteAdminProviderCreateChar() {
3290
+ const create = this.getState().admin?.modelProviders?.create;
3291
+ if (!create?.editing || create.saving) return;
3292
+ const next = deletePromptCharBackward(create.draft || "", create.cursorIndex || 0);
3293
+ this.setAdminProviderCreateDraft(next.prompt, next.cursor);
3294
+ }
3295
+
3296
+ moveAdminProviderCreateCursor(delta) {
3297
+ const create = this.getState().admin?.modelProviders?.create;
3298
+ if (!create?.editing || create.saving) return;
3299
+ const draft = create.draft || "";
3300
+ this.setAdminProviderCreateDraft(draft, clampPromptCursor(draft, (create.cursorIndex || 0) + delta));
3301
+ }
3302
+
3303
+ moveAdminProviderCreateCursorToBoundary(kind) {
3304
+ const create = this.getState().admin?.modelProviders?.create;
3305
+ if (!create?.editing || create.saving) return;
3306
+ const draft = create.draft || "";
3307
+ this.setAdminProviderCreateDraft(draft, kind === "start" ? 0 : draft.length);
3308
+ }
3309
+
3310
+ advanceAdminProviderCreate() {
3311
+ const create = this.getState().admin?.modelProviders?.create;
3312
+ if (!create?.editing || create.saving || create.stage !== "name") return;
3313
+ const name = String(create.draft || "").trim();
3314
+ if (!name || name.length > 64 || !/^[A-Za-z0-9._-]+$/u.test(name)) {
3315
+ this.dispatch({ type: "admin/modelProviders/createFailed", error: "Use 1-64 letters, numbers, dots, dashes, or underscores for the provider name." });
3316
+ return;
3317
+ }
3318
+ this.dispatch({ type: "admin/modelProviders/createCredential", name });
3319
+ }
3320
+
3321
+ async saveAdminProviderCreate() {
3322
+ const create = this.getState().admin?.modelProviders?.create;
3323
+ if (!create?.editing || create.saving || create.stage !== "credential") return;
3324
+ const credential = String(create.draft || "").trim();
3325
+ if (!credential) {
3326
+ this.dispatch({ type: "admin/modelProviders/createFailed", error: "Enter a provider credential." });
3327
+ return;
3328
+ }
3329
+ const input = {
3330
+ name: create.name,
3331
+ type: create.typeId,
3332
+ credentials: /github/i.test(create.typeId) ? { githubToken: credential } : { apiKey: credential },
3333
+ shared: create.shared === true,
3334
+ };
3335
+ // Clear the credential from shared state before any network await.
3336
+ this.dispatch({ type: "admin/modelProviders/createSaving" });
3337
+ const outcome = await this.createAdminProvider(input);
3338
+ this.dispatch(outcome?.ok
3339
+ ? { type: "admin/modelProviders/createEnd" }
3340
+ : { type: "admin/modelProviders/createFailed", error: outcome?.error });
3341
+ }
3342
+
3343
+ cancelAdminProviderCreate() {
3344
+ this.dispatch({ type: "admin/modelProviders/createEnd" });
3345
+ }
3346
+
3347
+ setAdminModelProviderSelection({ focus, providerName, agentId } = {}) {
3348
+ this.dispatch({ type: "admin/modelProviders/select", focus, providerName, agentId });
3349
+ }
3350
+
3351
+ setAdminModelProviderPage(page) {
3352
+ if (page === "shared" && !this.getState().admin?.profile?.isAdmin) return;
3353
+ this.dispatch({ type: "admin/modelProviders/page", page });
3354
+ }
3355
+
3356
+ stepAdminModelProviderSelection(delta) {
3357
+ const view = selectAdminConsole(this.getState()).modelProviders;
3358
+ const focus = view.selection?.focus === "agents" ? "agents" : "providers";
3359
+ const rows = focus === "agents"
3360
+ ? view.systemAgentRoutes
3361
+ : (view.page === "shared" ? view.sharedProviders : view.myProviders);
3362
+ if (!rows.length) return;
3363
+ const selectedId = focus === "agents" ? view.selection?.agentId : view.selection?.providerName;
3364
+ let index = rows.findIndex((row) => (focus === "agents" ? row.agentId : row.name) === selectedId);
3365
+ if (index < 0) index = 0;
3366
+ const next = rows[(index + delta + rows.length) % rows.length];
3367
+ this.setAdminModelProviderSelection(focus === "agents"
3368
+ ? { focus, agentId: next.agentId }
3369
+ : { focus, providerName: next.name });
3370
+ }
3371
+
3372
+ async toggleSelectedAdminProviderSystemUse() {
3373
+ const view = selectAdminConsole(this.getState()).modelProviders;
3374
+ const provider = view.myProviders.find((row) => row.name === view.selection?.providerName);
3375
+ if (!provider || !this.getState().admin?.profile?.isAdmin) return;
3376
+ return this.setAdminProviderSystemUse(provider.name, !provider.systemUseEnabled);
3377
+ }
3378
+
3379
+ async deleteSelectedAdminProvider() {
3380
+ const providers = selectAdminConsole(this.getState()).modelProviders;
3381
+ const selected = [...providers.myProviders, ...providers.sharedProviders]
3382
+ .find((provider) => provider.name === providers.selection?.providerName);
3383
+ if (!selected) return;
3384
+ return this.deleteAdminProvider(selected.name, { shared: selected.class === "shared" });
3385
+ }
3386
+
3387
+ requestDeleteSelectedAdminProvider() {
3388
+ const providers = selectAdminConsole(this.getState()).modelProviders;
3389
+ const selected = [...providers.myProviders, ...providers.sharedProviders]
3390
+ .find((provider) => provider.name === providers.selection?.providerName);
3391
+ if (!selected) return;
3392
+ this.dispatch({
3393
+ type: "ui/modal",
3394
+ modal: {
3395
+ type: "confirm",
3396
+ title: "Delete Model Provider",
3397
+ message: `Delete ${selected.name}? Sessions using it will wait until that provider name exists again.`,
3398
+ confirmLabel: "Delete Provider",
3399
+ action: "deleteAdminProvider",
3400
+ previousFocus: FOCUS_REGIONS.ADMIN,
3401
+ },
3402
+ });
3403
+ }
3404
+
3405
+ async cycleAdminModelDefault(scope, { restartDisposition = null } = {}) {
3406
+ const providers = selectAdminConsole(this.getState()).modelProviders;
3407
+ const choices = scope === "user" ? providers.userChoices
3408
+ : scope === "cluster" ? providers.clusterChoices : providers.systemChoices;
3409
+ const current = scope === "user" ? providers.mySessionDefault?.configured?.model
3410
+ : scope === "cluster" ? providers.clusterSessionDefault?.configured?.model
3411
+ : providers.systemSessionDefault?.configured?.model;
3412
+ const values = [null, ...choices];
3413
+ const index = Math.max(0, values.findIndex((choice) => choice?.qualifiedName === current));
3414
+ const next = values[(index + 1) % values.length];
3415
+ if (scope === "system") return this.setAdminSystemModelDefault(next, { restartDisposition });
3416
+ return this.setAdminModelDefault(scope, next);
3417
+ }
3418
+
3419
+ async cycleSelectedAdminSystemAgentOverride() {
3420
+ const providers = selectAdminConsole(this.getState()).modelProviders;
3421
+ const route = providers.systemAgentRoutes.find((row) => row.agentId === providers.selection?.agentId)
3422
+ || providers.systemAgentRoutes[0];
3423
+ if (!route) return;
3424
+ const values = [null, ...providers.systemChoices];
3425
+ const current = route.override?.model || null;
3426
+ const index = Math.max(0, values.findIndex((choice) => choice?.qualifiedName === current));
3427
+ const next = values[(index + 1) % values.length];
3428
+ return next
3429
+ ? this.setAdminSystemAgentModel(route.agentId, next)
3430
+ : this.clearAdminSystemAgentModel(route.agentId);
3431
+ }
3432
+
3013
3433
  // ── Agent packages (Admin → Agents) ──────────────────────────
3014
3434
 
3015
3435
  setAdminSection(section) {
3016
3436
  this.dispatch({ type: "admin/section", section });
3437
+ if (section === "providers") {
3438
+ const providers = this.getState().admin?.modelProviders;
3439
+ if (!providers?.fetchedAt) void this.refreshAdminModelProviders().catch(() => {});
3440
+ }
3017
3441
  if (section === "packages") {
3018
3442
  const pkgs = this.getState().admin?.packages;
3019
3443
  if (!pkgs?.fetchedAt) void this.refreshAdminAgentPackages().catch(() => {});
@@ -3217,6 +3641,7 @@ export class PilotSwarmUiController {
3217
3641
  case "republish":
3218
3642
  await this.transport.republishAgentPackageVersion(
3219
3643
  name, arg, selector?.scope === "shared" ? "user" : "shared",
3644
+ selector,
3220
3645
  );
3221
3646
  break;
3222
3647
  case "pin":
@@ -3471,6 +3896,468 @@ export class PilotSwarmUiController {
3471
3896
  }
3472
3897
  }
3473
3898
 
3899
+ // ─── Providers & Budgets (the coin button) ─────────────────
3900
+ //
3901
+ // docs/proposals/providers-and-budgets-meters.md is the build spec: one
3902
+ // table over one read, plus the day-by-day chart under the row someone
3903
+ // selected. Nothing here asks whether the viewer may do a thing. Every
3904
+ // call carries the signed-in person down to the `cms_provider_*`
3905
+ // procedures, the DATABASE refuses what they may not do, and it writes the
3906
+ // refusal in words meant for them — which are passed straight through,
3907
+ // because they name the remedy and nothing this file could invent would.
3908
+
3909
+ /**
3910
+ * Open the surface and load it.
3911
+ *
3912
+ * `provider` lets one call land on the provider that stopped something —
3913
+ * the paused line's link, where the sentence is above the table and the
3914
+ * row it names is in it.
3915
+ */
3916
+ async openBudget({ provider = undefined } = {}) {
3917
+ this.dispatch({
3918
+ type: "ui/budgetOpen",
3919
+ open: true,
3920
+ ...(provider === undefined ? {} : { provider }),
3921
+ });
3922
+ await this.loadProviderTable();
3923
+ // A provider opened AT a name shows that name's chart straight away.
3924
+ const opened = this.getState().budget || {};
3925
+ if (opened.selectedProvider) {
3926
+ await Promise.all([
3927
+ this.loadProviderSeries(opened.selectedProvider, opened.selectedScope || "*"),
3928
+ this.loadProviderSystemUsage(opened.selectedProvider, opened.selectedScope || "*"),
3929
+ ]);
3930
+ }
3931
+ }
3932
+
3933
+ /** Close the surface and return to the standard workspace. */
3934
+ closeBudget() {
3935
+ this.dispatch({ type: "ui/budgetOpen", open: false });
3936
+ }
3937
+
3938
+ /**
3939
+ * The "show overall usage" tick.
3940
+ *
3941
+ * Both pairs of numbers are already in the grid, so this changes what the
3942
+ * cells print and costs no request. Omit `overall` to flip it.
3943
+ */
3944
+ setBudgetOverall(overall) {
3945
+ const before = this.getState().budget?.overall === true;
3946
+ this.dispatch({ type: "budget/overall", ...(typeof overall === "boolean" ? { overall } : {}) });
3947
+ const budget = this.getState().budget || {};
3948
+ // The cells already hold both pairs, so the TABLE costs no request.
3949
+ // The chart does: its bars are one scope or the other, and leaving
3950
+ // the old ones up relabels one person's spend as everyone's.
3951
+ if (budget.overall === before || !budget.selectedProvider) return undefined;
3952
+ return this.loadProviderSeries(budget.selectedProvider, budget.selectedScope || "*");
3953
+ }
3954
+
3955
+ async setBudgetRangeDays(days) {
3956
+ const rangeDays = Number(days);
3957
+ if (!BUDGET_SERIES_RANGES.includes(rangeDays)) return;
3958
+ const before = this.getState().budget?.rangeDays ?? BUDGET_SERIES_DAYS;
3959
+ this.dispatch({ type: "budget/rangeDays", rangeDays });
3960
+ const budget = this.getState().budget || {};
3961
+ if (rangeDays === before || !budget.selectedProvider) return;
3962
+ await Promise.all([
3963
+ this.loadProviderSeries(budget.selectedProvider, budget.selectedScope || "*"),
3964
+ this.loadProviderSystemUsage(budget.selectedProvider, budget.selectedScope || "*"),
3965
+ ]);
3966
+ }
3967
+
3968
+ /**
3969
+ * Select a row: a provider, or one of its per-model limits.
3970
+ *
3971
+ * `scope` is '*' for the provider itself, or a qualified model reference
3972
+ * for a model row — and the chart follows it, so standing on a model row
3973
+ * shows that model's days rather than the whole provider's. Selecting the
3974
+ * row you are already on clears it: a provider collapses, a model row
3975
+ * falls back to the provider above it.
3976
+ *
3977
+ * The model rows are already in state — expanding shows rows the table
3978
+ * has. Only the chart is a read.
3979
+ */
3980
+ async selectBudgetProvider(provider, scope = "*") {
3981
+ this.dispatch({ type: "budget/selectProvider", provider, scope });
3982
+ const budget = this.getState().budget || {};
3983
+ if (!budget.selectedProvider) return;
3984
+ await Promise.all([
3985
+ this.loadProviderSeries(budget.selectedProvider, budget.selectedScope || "*"),
3986
+ this.loadProviderSystemUsage(budget.selectedProvider, budget.selectedScope || "*"),
3987
+ ]);
3988
+ }
3989
+
3990
+ /**
3991
+ * The table: every provider in the viewer's namespace with its used and
3992
+ * quota figures for all three periods, plus what is waiting.
3993
+ *
3994
+ * Two reads, not five. The grid is one call by design — the numbers in a
3995
+ * row and the numbers in the row under it come from the same statement, so
3996
+ * they cannot be a refresh apart. The paused list is separate because it
3997
+ * also feeds the session rows, which say why a session stopped whether or
3998
+ * not this surface is open.
3999
+ *
4000
+ * A read that FAILS is recorded as a failure, never as empty data. The two
4001
+ * look identical on screen if you let them — "this provider has no limits"
4002
+ * and "we could not find out what its limits are" — and only one of them
4003
+ * is safe to act on. Whatever was already on screen stays there
4004
+ * underneath, marked old, rather than being blanked.
4005
+ */
4006
+ async loadProviderTable() {
4007
+ if (!this._budgetReadable()) return;
4008
+ this.dispatch({ type: "budget/loading" });
4009
+ const [grid, paused] = await Promise.all([
4010
+ this._readBudgetPart(() => this.transport.getProviderUsageGrid()),
4011
+ this._readBudgetPart(() => this.transport.listPausedSessions()),
4012
+ ]);
4013
+ this._settleBudgetRead([
4014
+ ["grid", "the provider table", grid, (v) => v?.rows || []],
4015
+ ["paused", "the paused sessions", paused, (v) => v?.sessions || []],
4016
+ ]);
4017
+ }
4018
+
4019
+ /**
4020
+ * The day-by-day chart under one provider.
4021
+ *
4022
+ * Kept apart from the table's read so a failed chart never blanks the
4023
+ * numbers above it, and so selecting a row does not re-read every
4024
+ * provider's meters.
4025
+ */
4026
+ /**
4027
+ * Re-read the table AND the chart under it.
4028
+ *
4029
+ * They are two requests, and re-reading only the first left the chart
4030
+ * saying "No token usage" beside a row reporting usage — which is the
4031
+ * no-data-versus-not-looked-again confusion this screen exists to
4032
+ * prevent. Both the Refresh button and the background poll come here.
4033
+ */
4034
+ async refreshProviderTable() {
4035
+ await this.loadProviderTable();
4036
+ const budget = this.getState().budget || {};
4037
+ if (budget.selectedProvider) {
4038
+ await Promise.all([
4039
+ this.loadProviderSeries(budget.selectedProvider, budget.selectedScope || "*"),
4040
+ this.loadProviderSystemUsage(budget.selectedProvider, budget.selectedScope || "*"),
4041
+ ]);
4042
+ }
4043
+ }
4044
+
4045
+ async loadProviderSeries(provider, scope = "*") {
4046
+ const name = String(provider ?? "").trim();
4047
+ if (!name || typeof this.transport.getProviderUsage !== "function") return;
4048
+ // '*' means the whole provider. Anything else is one model, and the
4049
+ // report is filtered to it — the same filter the model row's own
4050
+ // numbers came from, so the chart and the row agree.
4051
+ const model = scope && scope !== "*" ? scope : null;
4052
+ // The chart must be drawn from the SAME slice of spend as the row
4053
+ // above it, or the two contradict each other under one heading. Two
4054
+ // filters do that, and both were missing:
4055
+ //
4056
+ // chargeClass "user" — the meters a cell reads are never moved for
4057
+ // system spend, so a chart that summed every class drew a provider
4058
+ // as over its limit while the cell said 15%.
4059
+ //
4060
+ // mine — with the tick off a cell holds YOUR spend against YOUR
4061
+ // share, so an unfiltered chart put the whole fleet's bars under a
4062
+ // heading that said "your usage", above a line that was your
4063
+ // personal share of the limit.
4064
+ const overall = this.getState().budget?.overall === true;
4065
+ const rangeDays = this.getState().budget?.rangeDays ?? BUDGET_SERIES_DAYS;
4066
+ this.dispatch({ type: "budget/series/loading", provider: name, scope, rangeDays });
4067
+ try {
4068
+ const report = await this.transport.getProviderUsage({
4069
+ days: rangeDays,
4070
+ provider: name,
4071
+ ...(model ? { model } : {}),
4072
+ chargeClass: "user",
4073
+ ...(overall ? {} : { mine: true }),
4074
+ dimension: "provider",
4075
+ });
4076
+ this.dispatch({
4077
+ type: "budget/series/loaded",
4078
+ provider: name,
4079
+ scope,
4080
+ rangeDays,
4081
+ days: Array.isArray(report?.daily) ? report.daily : [],
4082
+ });
4083
+ } catch (error) {
4084
+ this.dispatch({
4085
+ type: "budget/series/failed",
4086
+ provider: name, scope, rangeDays, error: budgetRefusalMessage(error),
4087
+ });
4088
+ }
4089
+ }
4090
+
4091
+ /** Admin-only machinery spend, kept separate from user limits. */
4092
+ async loadProviderSystemUsage(provider, scope = "*") {
4093
+ const role = this.getState().auth?.authorization?.role;
4094
+ const isAdmin = role === "admin" || role === "anonymous"
4095
+ || this.getState().admin?.profile?.isAdmin === true;
4096
+ const name = String(provider ?? "").trim();
4097
+ if (!isAdmin || !name || typeof this.transport.getProviderUsage !== "function") return;
4098
+ const model = scope && scope !== "*" ? scope : null;
4099
+ const rangeDays = this.getState().budget?.rangeDays ?? BUDGET_SERIES_DAYS;
4100
+ this.dispatch({ type: "budget/systemUsage/loading", provider: name, scope, rangeDays });
4101
+ try {
4102
+ const report = await this.transport.getProviderUsage({
4103
+ days: rangeDays,
4104
+ provider: name,
4105
+ ...(model ? { model } : {}),
4106
+ chargeClass: "system",
4107
+ dimension: "model",
4108
+ limit: 100,
4109
+ });
4110
+ this.dispatch({ type: "budget/systemUsage/loaded", provider: name, scope, rangeDays, report });
4111
+ } catch (error) {
4112
+ this.dispatch({
4113
+ type: "budget/systemUsage/failed",
4114
+ provider: name, scope, rangeDays, error: budgetRefusalMessage(error),
4115
+ });
4116
+ }
4117
+ }
4118
+
4119
+ /**
4120
+ * Why the waiting sessions are waiting, for the session list.
4121
+ *
4122
+ * The catalog row carries a status of "waiting" and nothing else; the
4123
+ * reason is published by `listPausedSessions`, and the session list joins
4124
+ * the two (see budgetPauseForSession). So take them while a wait is on
4125
+ * screen — once a minute, and never while the Budget surface is open,
4126
+ * which runs its own refresh on the same cadence.
4127
+ *
4128
+ * A failure is no news: the reasons already on screen stay as they are.
4129
+ */
4130
+ async refreshBudgetPausesIfStale(sessions) {
4131
+ if (typeof this.transport.listPausedSessions !== "function") return;
4132
+ const state = this.getState();
4133
+ if (state.ui?.budgetOpen) return;
4134
+ // A scheduled session is "waiting" on its own clock, not on a budget,
4135
+ // and on a busy fleet those are most of the waits. Skipping them keeps
4136
+ // this from becoming a permanent background poll on a portal where
4137
+ // nothing is actually stopped.
4138
+ const waiting = (Array.isArray(sessions) ? sessions : [])
4139
+ .some((row) => row && !row.isGroup && row.status === "waiting" && row.cronActive !== true);
4140
+ // Nothing is waiting, and there is nothing on screen left to clear.
4141
+ if (!waiting && (state.budget?.paused || []).length === 0) return;
4142
+ const now = Date.now();
4143
+ if (now - (this.budgetPauseReadAt || 0) < 60_000) return;
4144
+ this.budgetPauseReadAt = now;
4145
+ await this.refreshPausedSessions({ quiet: true }).catch(() => {});
4146
+ }
4147
+
4148
+ /**
4149
+ * The paused list alone. One call, because the session rows need the
4150
+ * reasons whether or not the table is on screen.
4151
+ *
4152
+ * `quiet` is for the background poll: a minute-by-minute read that nobody
4153
+ * asked for must not put an error banner on a table the reader is not even
4154
+ * looking at. The reasons already on screen stay as they are, and the next
4155
+ * poll tries again.
4156
+ */
4157
+ async refreshPausedSessions({ quiet = false } = {}) {
4158
+ if (typeof this.transport.listPausedSessions !== "function") {
4159
+ if (quiet) return;
4160
+ this.dispatch({
4161
+ type: "budget/loadFailed",
4162
+ error: "Providers and budgets are not available on this deployment.",
4163
+ });
4164
+ return;
4165
+ }
4166
+ const paused = await this._readBudgetPart(() => this.transport.listPausedSessions());
4167
+ if (paused.error && quiet) return;
4168
+ this._settleBudgetRead([
4169
+ ["paused", "the paused sessions", paused, (v) => v?.sessions || []],
4170
+ ]);
4171
+ }
4172
+
4173
+ /** An older server has none of these routes. Say so; do not draw zeros. */
4174
+ _budgetReadable() {
4175
+ if (typeof this.transport.getProviderUsageGrid === "function") return true;
4176
+ this.dispatch({
4177
+ type: "budget/loadFailed",
4178
+ error: "Providers and budgets are not available on this deployment.",
4179
+ });
4180
+ return false;
4181
+ }
4182
+
4183
+ /** One read, kept apart from its siblings so one failure cannot take them all. */
4184
+ async _readBudgetPart(run) {
4185
+ try {
4186
+ return { value: await run(), error: null };
4187
+ } catch (error) {
4188
+ return { value: null, error: budgetRefusalMessage(error) };
4189
+ }
4190
+ }
4191
+
4192
+ /**
4193
+ * Turn the reads into state.
4194
+ *
4195
+ * Whatever came back is written; whatever failed is named. A partial
4196
+ * failure is worth the two dispatches: the parts that arrived are worth
4197
+ * having, and the part that did not arrive has to say so rather than
4198
+ * showing the last numbers as if they were current.
4199
+ */
4200
+ _settleBudgetRead(parts) {
4201
+ const loaded = {};
4202
+ const failures = [];
4203
+ for (const [key, label, read, pick] of parts) {
4204
+ if (read.error) failures.push({ label, error: read.error });
4205
+ else loaded[key] = pick(read.value);
4206
+ }
4207
+ if (Object.keys(loaded).length === 0) {
4208
+ // Everything failed, which nearly always means one cause — signed
4209
+ // out, server down. Two copies of one sentence is not two facts,
4210
+ // so the server's own words go up once.
4211
+ this.dispatch({ type: "budget/loadFailed", error: failures[0]?.error || "The read failed." });
4212
+ return;
4213
+ }
4214
+ this.dispatch({ type: "budget/loaded", ...loaded });
4215
+ if (failures.length === 0) return;
4216
+ const message = failures.map((f) => `${f.label} could not be read: ${f.error}`).join(" ");
4217
+ // WHICH half failed decides what is marked. `budget/loadFailed` sets
4218
+ // the TABLE's error, and firing it when the paused list alone failed
4219
+ // put a red "these numbers are from an earlier read" over numbers
4220
+ // that had just been read successfully — while the genuinely old
4221
+ // waiting line underneath carried no mark at all. Exactly backwards.
4222
+ const gridFailed = parts.some(([key, , read]) => key === "grid" && read.error);
4223
+ this.dispatch(gridFailed
4224
+ ? { type: "budget/loadFailed", error: message }
4225
+ : { type: "budget/pausedFailed", error: message });
4226
+ }
4227
+
4228
+ /**
4229
+ * Every change to a provider, in one shape: run it, re-read the table, and
4230
+ * hand the caller back what happened.
4231
+ *
4232
+ * Nothing throws. A sheet gets `{ok, error, code, result}` and renders the
4233
+ * refusal where the person is looking; the same message also goes to the
4234
+ * status line for anyone who has already closed the sheet.
4235
+ */
4236
+ async _runBudgetChange(run) {
4237
+ try {
4238
+ const result = await run();
4239
+ await this.loadProviderTable();
4240
+ const after = this.getState().budget || {};
4241
+ // A limit that changed moves the chart's dashed line, so the chart
4242
+ // is re-read with the table rather than left describing the old one.
4243
+ if (after.selectedProvider) {
4244
+ await this.loadProviderSeries(after.selectedProvider, after.selectedScope || "*");
4245
+ }
4246
+ return { ok: true, error: null, code: null, result: result ?? null };
4247
+ } catch (error) {
4248
+ const message = budgetRefusalMessage(error);
4249
+ this.dispatch({ type: "ui/status", text: message });
4250
+ // A refusal is often the world having moved — the provider was
4251
+ // already deleted, the limit already gone. Re-reading here is
4252
+ // what stops the table keeping a row the server says is not
4253
+ // there, with a chart under it saying "no usage".
4254
+ await this.loadProviderTable().catch(() => {});
4255
+ return { ok: false, error: message, code: error?.code || null, result: null };
4256
+ }
4257
+ }
4258
+
4259
+ /** A change this deployment cannot make at all, reported like any other refusal. */
4260
+ _refuseBudgetChange(message) {
4261
+ this.dispatch({ type: "ui/status", text: message });
4262
+ return { ok: false, error: message, code: "UNSUPPORTED", result: null };
4263
+ }
4264
+
4265
+ /**
4266
+ * Create a provider. `shared` makes one anyone may spend from (admins
4267
+ * only); otherwise it is the caller's own, on their own credentials, and
4268
+ * nobody else sees it. The name is permanent — sessions refer to it.
4269
+ */
4270
+ async createProvider({ name, type, credentials, baseUrl = null, shared = false } = {}) {
4271
+ const op = shared ? "createProvider" : "createMyProvider";
4272
+ if (typeof this.transport[op] !== "function") {
4273
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4274
+ }
4275
+ return this._runBudgetChange(() => this.transport[op]({ name, type, credentials, baseUrl }));
4276
+ }
4277
+
4278
+ /**
4279
+ * Delete a provider. Which door it goes through follows the provider's
4280
+ * own class, read from the table already on screen: a shared provider is
4281
+ * an admin's to remove, a personal one is its owner's. The result carries
4282
+ * `waitingSessions` — how many sessions now wait on a name that no longer
4283
+ * resolves — which the sheet says out loud before it is confirmed.
4284
+ */
4285
+ async deleteProvider(name, { shared = null } = {}) {
4286
+ const known = (this.getState().budget?.grid || [])
4287
+ .find((row) => row?.providerName === name && row?.rowKind !== "model");
4288
+ const isShared = shared === null ? known?.class === "shared" : shared === true;
4289
+ const op = isShared ? "deleteProvider" : "deleteMyProvider";
4290
+ if (typeof this.transport[op] !== "function") {
4291
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4292
+ }
4293
+ return this._runBudgetChange(() => this.transport[op](name));
4294
+ }
4295
+
4296
+ /**
4297
+ * Save one limit. The same (period, scope) replaces what was there, and
4298
+ * the limit counts from what the period has ALREADY spent — it never
4299
+ * resets anything. The result's `seededTokens` is that already-counted
4300
+ * number, which the editor uses to warn that saving pauses sessions now.
4301
+ */
4302
+ async setProviderLimit({ provider, period, model = null, tokens } = {}) {
4303
+ if (typeof this.transport.setProviderLimit !== "function") {
4304
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4305
+ }
4306
+ return this._runBudgetChange(() => this.transport.setProviderLimit({ name: provider, period, model, tokens }));
4307
+ }
4308
+
4309
+ /** Drop one limit. Removing one only ever grants room; usage history is kept. */
4310
+ async removeProviderLimit({ provider, period, model = null } = {}) {
4311
+ if (typeof this.transport.removeProviderLimit !== "function") {
4312
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4313
+ }
4314
+ return this._runBudgetChange(() => this.transport.removeProviderLimit({ name: provider, period, model }));
4315
+ }
4316
+
4317
+ /**
4318
+ * The share of each of a shared provider's limits one person may use.
4319
+ * 100 means no per-person ceiling. It is derived live from the limit as
4320
+ * it stands, so there is nothing to re-stamp when a limit changes.
4321
+ */
4322
+ async setProviderAllowance({ provider, pct } = {}) {
4323
+ if (typeof this.transport.setProviderAllowance !== "function") {
4324
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4325
+ }
4326
+ return this._runBudgetChange(() => this.transport.setProviderAllowance({ name: provider, pct }));
4327
+ }
4328
+
4329
+ /**
4330
+ * Place or release a hold. A hold stops new turns against a provider
4331
+ * until it is released, independently of every limit. With neither an end
4332
+ * time nor `release` it has no end.
4333
+ */
4334
+ async setProviderHold({ provider, untilUtc = null, release = false } = {}) {
4335
+ if (typeof this.transport.setProviderHold !== "function") {
4336
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4337
+ }
4338
+ return this._runBudgetChange(() => this.transport.setProviderHold({ name: provider, untilUtc, release }));
4339
+ }
4340
+
4341
+ /**
4342
+ * The cluster default tuple: what system sessions run, and what anyone
4343
+ * who set no default of their own gets. It must name a SHARED provider,
4344
+ * and the model half must belong to it — the database refuses anything
4345
+ * else, in words.
4346
+ */
4347
+ async setClusterDefault(tuple) {
4348
+ if (typeof this.transport.setClusterDefault !== "function") {
4349
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4350
+ }
4351
+ return this._runBudgetChange(() => this.transport.setClusterDefault(tuple));
4352
+ }
4353
+
4354
+ /** The caller's own prefill for new sessions. A null tuple clears it. */
4355
+ async setMyDefault(tuple) {
4356
+ if (typeof this.transport.setMyDefault !== "function") {
4357
+ return this._refuseBudgetChange("Providers and budgets are not available on this deployment.");
4358
+ }
4359
+ return this._runBudgetChange(() => this.transport.setMyDefault(tuple));
4360
+ }
3474
4361
  async ensureExecutionHistory(sessionId, { force = false } = {}) {
3475
4362
  if (!sessionId || typeof this.transport.getExecutionHistory !== "function") return null;
3476
4363
  const current = this.getState().executionHistory?.bySessionId?.[sessionId] || null;
@@ -5350,6 +6237,21 @@ export class PilotSwarmUiController {
5350
6237
  this.dispatch({ type: "ui/status", text: "Select a context window and press Enter" });
5351
6238
  }
5352
6239
 
6240
+ /**
6241
+ * The model step of the create flow.
6242
+ *
6243
+ * What it lists is PROVIDERS, one group each, with the models their type
6244
+ * offers — so a provider an admin or a user created a minute ago is on
6245
+ * the list without anyone restarting anything, and the entry carries its
6246
+ * budget state inline ("42% of its Daily limit used", "AT ITS DAILY
6247
+ * LIMIT") so the choice is made knowing.
6248
+ *
6249
+ * An empty namespace REFUSES rather than falling back to the whole
6250
+ * catalog: a list of models nothing can pay for is a list of dead ends.
6251
+ * A deployment that has no provider operations at all — or one whose
6252
+ * provider read failed, which is not the same thing as having none — is
6253
+ * a different case and keeps exactly today's behaviour.
6254
+ */
5353
6255
  async openModelPicker(sessionOptions = {}) {
5354
6256
  if (typeof this.transport.listModels !== "function") {
5355
6257
  await this.openSessionAgentPicker(sessionOptions);
@@ -5362,6 +6264,170 @@ export class PilotSwarmUiController {
5362
6264
  return;
5363
6265
  }
5364
6266
 
6267
+ const namespace = await this._resolveProviderNamespace();
6268
+ const built = namespace?.error
6269
+ ? { refusal: namespace.error }
6270
+ : namespace
6271
+ ? this._buildProviderPickerGroups(namespace, models, sessionOptions)
6272
+ : await this._buildCatalogPickerGroups(models, sessionOptions);
6273
+ if (built.refusal) {
6274
+ this.dispatch({ type: "ui/status", text: built.refusal });
6275
+ return;
6276
+ }
6277
+ const { items, groups, selectedIndex } = built;
6278
+ this.dispatch({
6279
+ type: "ui/modal",
6280
+ modal: {
6281
+ type: "modelPicker",
6282
+ title: sessionOptions?.mode === "switchModel" ? "Switch model for session" : "Select model for new session",
6283
+ items,
6284
+ groups,
6285
+ selectedIndex,
6286
+ previousFocus: this.getState().ui.focusRegion,
6287
+ sessionOptions,
6288
+ },
6289
+ });
6290
+ this.dispatch({ type: "ui/status", text: "Select a model and press Enter" });
6291
+ }
6292
+
6293
+ /**
6294
+ * The providers this viewer can spend from, with their limits and the
6295
+ * default tuples. Null means the deployment predates runtime-provider
6296
+ * APIs. Once those APIs exist, a failed namespace read is authoritative:
6297
+ * fail closed rather than offering static type names with no proven
6298
+ * credential or ownership.
6299
+ */
6300
+ async _resolveProviderNamespace() {
6301
+ if (typeof this.transport.listProviders !== "function") return null;
6302
+ try {
6303
+ const [list, status, defaults] = await Promise.all([
6304
+ this.transport.listProviders(),
6305
+ typeof this.transport.getProviderStatus === "function"
6306
+ ? this.transport.getProviderStatus().catch(() => null)
6307
+ : null,
6308
+ typeof this.transport.getDefaults === "function"
6309
+ ? this.transport.getDefaults().catch(() => null)
6310
+ : null,
6311
+ ]);
6312
+ const providers = Array.isArray(list?.providers) ? list.providers : null;
6313
+ if (!providers) return { error: "Runtime provider availability could not be verified. Refresh and try again." };
6314
+ return {
6315
+ providers,
6316
+ status: Array.isArray(status?.providers) ? status.providers : [],
6317
+ statusKnown: Array.isArray(status?.providers),
6318
+ defaults: defaults || null,
6319
+ };
6320
+ } catch {
6321
+ return { error: "Runtime provider availability could not be verified. Refresh and try again." };
6322
+ }
6323
+ }
6324
+
6325
+ /**
6326
+ * One group per provider, models re-qualified under the provider's own
6327
+ * name.
6328
+ *
6329
+ * The catalog's `providerId` is the TYPE ("azure-openai"); a provider is
6330
+ * an instance of that type with a name of its own ("azure-prod"), and a
6331
+ * session runs `<provider>:<model>`. A deployment that never created a
6332
+ * second instance sees no change at all, because the seed names each
6333
+ * provider after the config entry it came from.
6334
+ */
6335
+ _buildProviderPickerGroups(namespace, models, sessionOptions) {
6336
+ const usable = namespace.providers.filter((row) => row.usableByMe !== false);
6337
+ if (usable.length === 0) {
6338
+ return {
6339
+ refusal: "No provider can pay for a session yet — add one of your own, or ask an admin for a shared one.",
6340
+ };
6341
+ }
6342
+ const modelsByType = new Map();
6343
+ for (const model of models) {
6344
+ const typeId = model?.providerId;
6345
+ if (!typeId) continue;
6346
+ if (!modelsByType.has(typeId)) modelsByType.set(typeId, []);
6347
+ modelsByType.get(typeId).push(model);
6348
+ }
6349
+ const statusByName = new Map(namespace.status.map((row) => [row.name, row]));
6350
+ const now = Date.now();
6351
+ // Your own default first, then the cluster's, then your own
6352
+ // providers, then the shared ones: what a plain Enter charges should
6353
+ // be what the deployment would have chosen anyway.
6354
+ const ranked = [...usable].sort((a, b) => {
6355
+ const rank = (row) => (row.isMyDefault === true ? 0
6356
+ : row.isClusterDefault === true ? 1
6357
+ : row.class === "personal" ? 2 : 3);
6358
+ return rank(a) - rank(b) || String(a.name).localeCompare(String(b.name));
6359
+ });
6360
+ const preferredModel = sessionOptions?.model
6361
+ || namespace.defaults?.mine?.model
6362
+ || namespace.defaults?.cluster?.model
6363
+ || (typeof this.transport.getDefaultModel === "function" ? this.transport.getDefaultModel() : undefined);
6364
+
6365
+ const items = [];
6366
+ const groups = ranked
6367
+ .map((provider) => {
6368
+ const room = providerPickerNote(provider, statusByName.get(provider.name) || null, now);
6369
+ // A provider with no credential cannot pay for anything. Say
6370
+ // so on the row rather than hiding it: the person who has to
6371
+ // fix it is often the one looking at the list.
6372
+ const disabledReason = provider.hasCredential === false
6373
+ ? `${provider.name} has no credential`
6374
+ : null;
6375
+ return {
6376
+ providerId: provider.name,
6377
+ providerType: provider.typeId,
6378
+ providerClass: provider.class,
6379
+ isClusterDefault: provider.isClusterDefault === true,
6380
+ isMyDefault: provider.isMyDefault === true,
6381
+ hasCredential: provider.hasCredential !== false,
6382
+ budgetState: room.state,
6383
+ budgetNote: room.text,
6384
+ models: (modelsByType.get(provider.name) || modelsByType.get(provider.typeId) || []).map((model) => {
6385
+ const qualifiedName = model.providerId === provider.name && model.qualifiedName
6386
+ ? model.qualifiedName
6387
+ : `${provider.name}:${model.modelName}`;
6388
+ const modelDisabledReason = disabledReason
6389
+ || (model.credentialAvailable === false
6390
+ ? `${provider.name} has no usable credential or endpoint`
6391
+ : null);
6392
+ const item = {
6393
+ id: qualifiedName,
6394
+ qualifiedName,
6395
+ modelName: model.modelName || qualifiedName,
6396
+ providerId: provider.name,
6397
+ providerType: provider.typeId,
6398
+ providerClass: provider.class,
6399
+ description: model.description || "",
6400
+ cost: model.cost || null,
6401
+ supportedReasoningEfforts: normalizeReasoningEfforts(model.supportedReasoningEfforts),
6402
+ defaultReasoningEffort: model.defaultReasoningEffort || null,
6403
+ supportedContextTiers: normalizeContextTiers(model.supportedContextTiers),
6404
+ defaultContextTier: model.defaultContextTier || null,
6405
+ contextWindowSizes: model.contextWindowSizes || null,
6406
+ isDefault: preferredModel === qualifiedName,
6407
+ budgetState: room.state,
6408
+ budgetNote: room.text,
6409
+ disabledReason: modelDisabledReason,
6410
+ disabled: Boolean(modelDisabledReason),
6411
+ };
6412
+ items.push(item);
6413
+ return item;
6414
+ }),
6415
+ };
6416
+ })
6417
+ .filter((group) => group.models.length > 0);
6418
+
6419
+ if (items.length === 0) {
6420
+ return { refusal: "No provider in your namespace offers a model to run." };
6421
+ }
6422
+ return { items, groups, selectedIndex: pickerSelectedIndex(items, preferredModel) };
6423
+ }
6424
+
6425
+ /**
6426
+ * The catalog list, exactly as it was before providers existed. Kept for
6427
+ * deployments whose server predates the provider operations. Provider API
6428
+ * failures never take this path.
6429
+ */
6430
+ async _buildCatalogPickerGroups(models, sessionOptions) {
5365
6431
  const defaultModel = typeof this.transport.getDefaultModel === "function"
5366
6432
  ? this.transport.getDefaultModel()
5367
6433
  : undefined;
@@ -5399,6 +6465,7 @@ export class PilotSwarmUiController {
5399
6465
  contextWindowSizes: model.contextWindowSizes || null,
5400
6466
  isDefault: defaultModel === model.qualifiedName,
5401
6467
  ghcpKeyMissing,
6468
+ disabledReason: ghcpKeyMissing ? "needs GitHub key" : null,
5402
6469
  disabled: ghcpKeyMissing,
5403
6470
  };
5404
6471
  items.push(item);
@@ -5407,26 +6474,11 @@ export class PilotSwarmUiController {
5407
6474
  }))
5408
6475
  .filter((group) => group.models.length > 0);
5409
6476
 
5410
- const preferredModel = sessionOptions?.model || defaultModel;
5411
- // Never preselect a key-blocked Copilot model: when the preferred
5412
- // model (often the catalog default) is unusable, land on the first
5413
- // usable model instead.
5414
- let selectedIndex = items.findIndex((model) => model.qualifiedName === preferredModel && !model.disabled);
5415
- if (selectedIndex < 0) selectedIndex = items.findIndex((model) => !model.disabled);
5416
- if (selectedIndex < 0) selectedIndex = 0;
5417
- this.dispatch({
5418
- type: "ui/modal",
5419
- modal: {
5420
- type: "modelPicker",
5421
- title: sessionOptions?.mode === "switchModel" ? "Switch model for session" : "Select model for new session",
5422
- items,
5423
- groups,
5424
- selectedIndex,
5425
- previousFocus: this.getState().ui.focusRegion,
5426
- sessionOptions,
5427
- },
5428
- });
5429
- this.dispatch({ type: "ui/status", text: "Select a model and press Enter" });
6477
+ return {
6478
+ items,
6479
+ groups,
6480
+ selectedIndex: pickerSelectedIndex(items, sessionOptions?.model || defaultModel),
6481
+ };
5430
6482
  }
5431
6483
 
5432
6484
  async _resolveGhcpUserKeySet() {
@@ -6767,6 +7819,8 @@ export class PilotSwarmUiController {
6767
7819
  await this.deleteSelectedArtifact({ confirmed: true });
6768
7820
  } else if (modal.action === "deleteMarkedArtifacts") {
6769
7821
  await this.deleteMarkedArtifacts({ confirmed: true });
7822
+ } else if (modal.action === "deleteAdminProvider") {
7823
+ await this.deleteSelectedAdminProvider();
6770
7824
  }
6771
7825
  return;
6772
7826
  }
@@ -6822,7 +7876,7 @@ export class PilotSwarmUiController {
6822
7876
  this.dispatch({
6823
7877
  type: "ui/status",
6824
7878
  text: item.ghcpKeyMissing
6825
- ? "This model needs a GitHub Copilot key — add yours in the Admin console first"
7879
+ ? "This model needs a GitHub Copilot provider — add your own in Admin Console first"
6826
7880
  : "This model is not available",
6827
7881
  });
6828
7882
  return;
@@ -9113,7 +10167,9 @@ export class PilotSwarmUiController {
9113
10167
  await this.refreshAdminProfile();
9114
10168
  return;
9115
10169
  case UI_COMMANDS.ADMIN_SHOW_GHCP:
9116
- this.setAdminSection("ghcp");
10170
+ // Compatibility command retained for older hosts; the visible
10171
+ // destination is now Model Providers.
10172
+ this.setAdminSection("providers");
9117
10173
  return;
9118
10174
  case UI_COMMANDS.ADMIN_SHOW_PACKAGES:
9119
10175
  this.setAdminSection("packages");