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
@@ -7,6 +7,7 @@ import { systemSessionSortOrder } from "./system-titles.js";
7
7
  import {
8
8
  normalizeArtifactEntries,
9
9
  normalizeSessionOwnerFilter,
10
+ normalizeSessionPause,
10
11
  normalizeStoredActiveSessionId,
11
12
  normalizeStoredCanvasPrefs,
12
13
  canvasKey,
@@ -16,6 +17,8 @@ import {
16
17
  normalizeStoredLayoutAdjustments,
17
18
  normalizeStoredPinnedSessionIds,
18
19
  normalizeStoredSessionOrder,
20
+ BUDGET_SERIES_DAYS,
21
+ BUDGET_SERIES_RANGES,
19
22
  createInitialState,
20
23
  } from "./state.js";
21
24
 
@@ -33,6 +36,12 @@ function packageResponseIsStale(packages, action) {
33
36
  return false;
34
37
  }
35
38
 
39
+ /** A provider name, or null. Names are cluster-unique and never blank. */
40
+ function normalizeProviderName(value) {
41
+ const name = String(value ?? "").trim();
42
+ return name ? name : null;
43
+ }
44
+
36
45
  function cloneHistoryMap(historyMap) {
37
46
  return new Map(historyMap);
38
47
  }
@@ -384,6 +393,13 @@ function computeRawSessionVisualStatus(session) {
384
393
  if (!session) return "unknown";
385
394
  const status = session.status || "unknown";
386
395
  const dormant = status === "waiting" || status === "idle" || status === "unknown";
396
+ // A budget pause outranks every other dormant reading. A session parked on
397
+ // a limit is not "waiting" in the sense the list means it — nothing is
398
+ // going to happen until a person changes a number — and the row is the
399
+ // only place most people will ever find that out.
400
+ if (dormant && normalizeSessionPause(session)) {
401
+ return "budget_paused";
402
+ }
387
403
  if (session.cronActive === true && dormant) {
388
404
  return "cron_waiting";
389
405
  }
@@ -2131,9 +2147,16 @@ export function appReducer(state, action) {
2131
2147
 
2132
2148
  case "admin/visibility": {
2133
2149
  const visible = Boolean(action.visible);
2134
- if (state.admin.visible === visible) return state;
2150
+ if (state.admin.visible === visible && !(visible && state.ui.budgetOpen)) return state;
2135
2151
  return {
2136
2152
  ...state,
2153
+ // The admin console and the Providers & Budgets surface take
2154
+ // the SAME slot — the workspace under the header. Opening the
2155
+ // console therefore has to put the budget away, or the console
2156
+ // button reads active while the budget is still what is on
2157
+ // screen and the click looks dead. `ui/budgetOpen` closes the
2158
+ // console; this is the other direction.
2159
+ ui: visible && state.ui.budgetOpen ? { ...state.ui, budgetOpen: false } : state.ui,
2137
2160
  admin: {
2138
2161
  ...state.admin,
2139
2162
  visible,
@@ -2143,6 +2166,19 @@ export function appReducer(state, action) {
2143
2166
  ghcpKey: visible
2144
2167
  ? state.admin.ghcpKey
2145
2168
  : { editing: false, draft: "", saving: false, error: null, lastSavedAt: state.admin.ghcpKey.lastSavedAt },
2169
+ modelProviders: visible
2170
+ ? state.admin.modelProviders
2171
+ : {
2172
+ ...state.admin.modelProviders,
2173
+ create: {
2174
+ ...state.admin.modelProviders.create,
2175
+ editing: false,
2176
+ draft: "",
2177
+ cursorIndex: 0,
2178
+ saving: false,
2179
+ error: null,
2180
+ },
2181
+ },
2146
2182
  },
2147
2183
  };
2148
2184
  }
@@ -2340,6 +2376,255 @@ export function appReducer(state, action) {
2340
2376
  },
2341
2377
  };
2342
2378
  }
2379
+ case "admin/modelProviders/loading": {
2380
+ return {
2381
+ ...state,
2382
+ admin: {
2383
+ ...state.admin,
2384
+ modelProviders: {
2385
+ ...state.admin.modelProviders,
2386
+ loading: true,
2387
+ error: null,
2388
+ },
2389
+ },
2390
+ };
2391
+ }
2392
+ case "admin/modelProviders/loaded": {
2393
+ return {
2394
+ ...state,
2395
+ admin: {
2396
+ ...state.admin,
2397
+ modelProviders: {
2398
+ ...state.admin.modelProviders,
2399
+ loading: false,
2400
+ error: null,
2401
+ providers: Array.isArray(action.providers) ? action.providers : [],
2402
+ models: Array.isArray(action.models) ? action.models : [],
2403
+ defaults: action.defaults || null,
2404
+ fetchedAt: Date.now(),
2405
+ },
2406
+ },
2407
+ };
2408
+ }
2409
+ case "admin/modelProviders/loadFailed": {
2410
+ return {
2411
+ ...state,
2412
+ admin: {
2413
+ ...state.admin,
2414
+ modelProviders: {
2415
+ ...state.admin.modelProviders,
2416
+ loading: false,
2417
+ error: action.error ? String(action.error) : "Failed to load model providers",
2418
+ },
2419
+ },
2420
+ };
2421
+ }
2422
+ case "admin/modelProviders/mutationPending": {
2423
+ return {
2424
+ ...state,
2425
+ admin: {
2426
+ ...state.admin,
2427
+ modelProviders: {
2428
+ ...state.admin.modelProviders,
2429
+ mutation: { pending: action.pending || "change", error: null },
2430
+ },
2431
+ },
2432
+ };
2433
+ }
2434
+ case "admin/modelProviders/mutationDone": {
2435
+ return {
2436
+ ...state,
2437
+ admin: {
2438
+ ...state.admin,
2439
+ modelProviders: {
2440
+ ...state.admin.modelProviders,
2441
+ mutation: { pending: null, error: null },
2442
+ },
2443
+ },
2444
+ };
2445
+ }
2446
+ case "admin/modelProviders/mutationFailed": {
2447
+ return {
2448
+ ...state,
2449
+ admin: {
2450
+ ...state.admin,
2451
+ modelProviders: {
2452
+ ...state.admin.modelProviders,
2453
+ mutation: {
2454
+ pending: null,
2455
+ error: action.error ? String(action.error) : "The provider change failed",
2456
+ },
2457
+ },
2458
+ },
2459
+ };
2460
+ }
2461
+ case "admin/modelProviders/select": {
2462
+ return {
2463
+ ...state,
2464
+ admin: {
2465
+ ...state.admin,
2466
+ modelProviders: {
2467
+ ...state.admin.modelProviders,
2468
+ selection: {
2469
+ focus: action.focus === "agents" ? "agents" : "providers",
2470
+ providerName: action.providerName ?? state.admin.modelProviders.selection?.providerName ?? null,
2471
+ agentId: action.agentId ?? state.admin.modelProviders.selection?.agentId ?? null,
2472
+ },
2473
+ },
2474
+ },
2475
+ };
2476
+ }
2477
+ case "admin/modelProviders/page": {
2478
+ const page = action.page === "shared" ? "shared" : "mine";
2479
+ if (page === state.admin.modelProviders.page) return state;
2480
+ return {
2481
+ ...state,
2482
+ admin: {
2483
+ ...state.admin,
2484
+ modelProviders: {
2485
+ ...state.admin.modelProviders,
2486
+ page,
2487
+ selection: {
2488
+ ...state.admin.modelProviders.selection,
2489
+ focus: "providers",
2490
+ providerName: null,
2491
+ },
2492
+ },
2493
+ },
2494
+ };
2495
+ }
2496
+ case "admin/modelProviders/createBegin": {
2497
+ const draft = String(action.name || "");
2498
+ return {
2499
+ ...state,
2500
+ admin: {
2501
+ ...state.admin,
2502
+ modelProviders: {
2503
+ ...state.admin.modelProviders,
2504
+ create: {
2505
+ editing: true,
2506
+ stage: "name",
2507
+ name: "",
2508
+ typeId: String(action.typeId || ""),
2509
+ shared: action.shared === true,
2510
+ draft,
2511
+ cursorIndex: draft.length,
2512
+ saving: false,
2513
+ error: null,
2514
+ },
2515
+ },
2516
+ },
2517
+ };
2518
+ }
2519
+ case "admin/modelProviders/createDraft": {
2520
+ const draft = String(action.draft || "").replace(/\r?\n/gu, "");
2521
+ const cursorIndex = Math.max(0, Math.min(Number(action.cursorIndex) || 0, draft.length));
2522
+ return {
2523
+ ...state,
2524
+ admin: {
2525
+ ...state.admin,
2526
+ modelProviders: {
2527
+ ...state.admin.modelProviders,
2528
+ create: { ...state.admin.modelProviders.create, draft, cursorIndex, error: null },
2529
+ },
2530
+ },
2531
+ };
2532
+ }
2533
+ case "admin/modelProviders/createType": {
2534
+ return {
2535
+ ...state,
2536
+ admin: {
2537
+ ...state.admin,
2538
+ modelProviders: {
2539
+ ...state.admin.modelProviders,
2540
+ create: {
2541
+ ...state.admin.modelProviders.create,
2542
+ typeId: String(action.typeId || ""),
2543
+ error: null,
2544
+ },
2545
+ },
2546
+ },
2547
+ };
2548
+ }
2549
+ case "admin/modelProviders/createCredential": {
2550
+ return {
2551
+ ...state,
2552
+ admin: {
2553
+ ...state.admin,
2554
+ modelProviders: {
2555
+ ...state.admin.modelProviders,
2556
+ create: {
2557
+ ...state.admin.modelProviders.create,
2558
+ stage: "credential",
2559
+ name: String(action.name || ""),
2560
+ draft: "",
2561
+ cursorIndex: 0,
2562
+ error: null,
2563
+ },
2564
+ },
2565
+ },
2566
+ };
2567
+ }
2568
+ case "admin/modelProviders/createSaving": {
2569
+ return {
2570
+ ...state,
2571
+ admin: {
2572
+ ...state.admin,
2573
+ modelProviders: {
2574
+ ...state.admin.modelProviders,
2575
+ create: {
2576
+ ...state.admin.modelProviders.create,
2577
+ draft: "",
2578
+ cursorIndex: 0,
2579
+ saving: true,
2580
+ error: null,
2581
+ },
2582
+ },
2583
+ },
2584
+ };
2585
+ }
2586
+ case "admin/modelProviders/createFailed": {
2587
+ const current = state.admin.modelProviders.create;
2588
+ const credentialStage = current.stage === "credential";
2589
+ return {
2590
+ ...state,
2591
+ admin: {
2592
+ ...state.admin,
2593
+ modelProviders: {
2594
+ ...state.admin.modelProviders,
2595
+ create: {
2596
+ ...current,
2597
+ draft: credentialStage ? "" : current.draft,
2598
+ cursorIndex: credentialStage ? 0 : current.cursorIndex,
2599
+ saving: false,
2600
+ error: action.error ? String(action.error) : "The provider could not be created",
2601
+ },
2602
+ },
2603
+ },
2604
+ };
2605
+ }
2606
+ case "admin/modelProviders/createEnd": {
2607
+ return {
2608
+ ...state,
2609
+ admin: {
2610
+ ...state.admin,
2611
+ modelProviders: {
2612
+ ...state.admin.modelProviders,
2613
+ create: {
2614
+ editing: false,
2615
+ stage: "name",
2616
+ name: "",
2617
+ typeId: "",
2618
+ shared: false,
2619
+ draft: "",
2620
+ cursorIndex: 0,
2621
+ saving: false,
2622
+ error: null,
2623
+ },
2624
+ },
2625
+ },
2626
+ };
2627
+ }
2343
2628
  case "admin/section": {
2344
2629
  const section = ["packages", "workers", "ghcp"].includes(action.section) ? action.section : "ghcp";
2345
2630
  return { ...state, admin: { ...state.admin, section } };
@@ -2870,6 +3155,369 @@ export function appReducer(state, action) {
2870
3155
  return { ...state, ui: { ...state.ui, diagnosticsOpen: open } };
2871
3156
  }
2872
3157
 
3158
+ // ── Providers & Budgets ─────────────────────────────────────────
3159
+ // The surface itself. It takes the workspace under the header — the
3160
+ // same slot as the admin console — so opening it closes the console
3161
+ // rather than stacking two full-screen surfaces.
3162
+ //
3163
+ // `open` may be omitted to mean "toggle". `provider` lets one dispatch
3164
+ // open the surface AT a provider, which is what the paused line's link
3165
+ // needs: it names the provider that stopped the sessions.
3166
+ case "ui/budgetOpen": {
3167
+ const open = typeof action.open === "boolean" ? action.open : !state.ui.budgetOpen;
3168
+ const provider = action.provider === undefined ? undefined : normalizeProviderName(action.provider);
3169
+ const budget = (open && provider !== undefined)
3170
+ ? { ...state.budget, selectedProvider: provider }
3171
+ : state.budget;
3172
+ const closesConsole = open && state.admin.visible;
3173
+ if (open === state.ui.budgetOpen && budget === state.budget && !closesConsole) return state;
3174
+ return {
3175
+ ...state,
3176
+ ui: { ...state.ui, budgetOpen: open },
3177
+ admin: closesConsole ? { ...state.admin, visible: false } : state.admin,
3178
+ budget,
3179
+ };
3180
+ }
3181
+
3182
+ case "budget/loading": {
3183
+ // A re-read while numbers are already on screen is a REFRESH: it
3184
+ // must not blank the report someone is reading. Only the very
3185
+ // first load gets to show an empty, loading surface.
3186
+ const refresh = action.refresh === true || state.budget.loaded === true;
3187
+ return {
3188
+ ...state,
3189
+ budget: {
3190
+ ...state.budget,
3191
+ loading: !refresh,
3192
+ refreshing: refresh,
3193
+ error: null,
3194
+ },
3195
+ };
3196
+ }
3197
+
3198
+ // One successful read. Both payloads are optional so a narrower
3199
+ // re-read — the paused list alone, on the background poll — replaces
3200
+ // only what it actually fetched.
3201
+ case "budget/loaded": {
3202
+ const budget = state.budget;
3203
+ const hasGrid = Array.isArray(action.grid);
3204
+ const grid = hasGrid ? action.grid : budget.grid;
3205
+ const dropped = hasGrid
3206
+ // A provider that is gone cannot stay selected: its rows and
3207
+ // its chart would be a name with nothing behind it.
3208
+ && budget.selectedProvider
3209
+ && !action.grid.some((row) => row?.providerName === budget.selectedProvider);
3210
+ const selectedProvider = dropped ? null : budget.selectedProvider;
3211
+ // A per-model limit can go while its provider stays — someone
3212
+ // removes that one limit, and its row goes with it. The SCOPE has
3213
+ // to be dropped for the same reason the provider is: otherwise
3214
+ // the chart keeps that model's bars on screen under the
3215
+ // provider's heading and the provider's limit line, which is one
3216
+ // subject's numbers labelled as another's.
3217
+ const wasScope = budget.selectedScope || "*";
3218
+ const scopeGone = hasGrid && !dropped && wasScope !== "*"
3219
+ && !action.grid.some((row) => row?.providerName === budget.selectedProvider
3220
+ && row?.rowKind === "model" && row?.scope === wasScope);
3221
+ const selectedScope = (dropped || scopeGone) ? "*" : wasScope;
3222
+ // But REMEMBER the name. A session stopped on a provider that no
3223
+ // longer exists sends its reader here by name, and clearing the
3224
+ // selection silently left them on a table with nothing selected
3225
+ // and no hint that the name they clicked is the missing thing.
3226
+ const missingProvider = dropped
3227
+ ? budget.selectedProvider
3228
+ : (hasGrid ? null : budget.missingProvider || null);
3229
+ return {
3230
+ ...state,
3231
+ budget: {
3232
+ ...budget,
3233
+ loading: false,
3234
+ refreshing: false,
3235
+ // `loaded` and `error` are claims about the TABLE, so only
3236
+ // a read that carried the table may change them. The
3237
+ // background poll reads the paused list alone; letting it
3238
+ // set `loaded` would turn an unread namespace into "you
3239
+ // have no providers", which is the one confusion this flag
3240
+ // exists to prevent.
3241
+ loaded: hasGrid ? true : budget.loaded,
3242
+ error: hasGrid ? null : budget.error,
3243
+ fetchedAt: hasGrid ? Date.now() : budget.fetchedAt,
3244
+ grid,
3245
+ paused: Array.isArray(action.paused) ? action.paused : budget.paused,
3246
+ pausedError: Array.isArray(action.paused) ? null : budget.pausedError || null,
3247
+ selectedProvider,
3248
+ selectedScope,
3249
+ missingProvider,
3250
+ // The chart belongs to a row that is no longer there —
3251
+ // either the provider or the one model limit under it.
3252
+ series: (selectedProvider === budget.selectedProvider && selectedScope === wasScope)
3253
+ ? budget.series
3254
+ : {
3255
+ provider: null, scope: "*", rangeDays: budget.rangeDays ?? BUDGET_SERIES_DAYS, days: [],
3256
+ loading: false, loaded: false, error: null, fetchedAt: 0,
3257
+ },
3258
+ systemUsage: (selectedProvider === budget.selectedProvider && selectedScope === wasScope)
3259
+ ? budget.systemUsage
3260
+ : {
3261
+ provider: null, scope: "*", rangeDays: budget.rangeDays ?? BUDGET_SERIES_DAYS,
3262
+ totals: null, days: [], breakdown: [],
3263
+ loading: false, loaded: false, error: null, fetchedAt: 0,
3264
+ },
3265
+ },
3266
+ };
3267
+ }
3268
+
3269
+ // Only the WAITING list failed. The table's own numbers were read
3270
+ // successfully in the same round, so nothing about them is stale and
3271
+ // nothing about them is marked — the waiting line is.
3272
+ case "budget/pausedFailed": {
3273
+ const message = action.error ? String(action.error?.message || action.error) : "";
3274
+ return {
3275
+ ...state,
3276
+ budget: {
3277
+ ...state.budget,
3278
+ loading: false,
3279
+ refreshing: false,
3280
+ pausedError: message || "Could not read what is waiting",
3281
+ },
3282
+ };
3283
+ }
3284
+
3285
+ // A read that FAILED. Distinct from a read that came back empty:
3286
+ // "this provider has no limits" and "we could not find out what its
3287
+ // limits are" are different facts, and only one of them is safe to act
3288
+ // on. So `loaded` and the previous numbers stay exactly as they were
3289
+ // and the error rides alongside them — the view says the read failed
3290
+ // rather than drawing zeros.
3291
+ case "budget/loadFailed": {
3292
+ const message = action.error ? String(action.error?.message || action.error) : "";
3293
+ return {
3294
+ ...state,
3295
+ budget: {
3296
+ ...state.budget,
3297
+ loading: false,
3298
+ refreshing: false,
3299
+ // The server's own words. It writes them for a person and
3300
+ // names the remedy, so passing them through beats any
3301
+ // "something went wrong" this file could invent.
3302
+ error: message || "Could not read providers and budgets",
3303
+ },
3304
+ };
3305
+ }
3306
+
3307
+ // The "show overall usage" tick. It changes which pair of numbers the
3308
+ // table prints, never which numbers were read: both pairs are already
3309
+ // in `grid`, so this is a render choice and costs no request.
3310
+ case "budget/overall": {
3311
+ const overall = typeof action.overall === "boolean" ? action.overall : !state.budget.overall;
3312
+ if (overall === state.budget.overall) return state;
3313
+ return { ...state, budget: { ...state.budget, overall } };
3314
+ }
3315
+
3316
+ case "budget/rangeDays": {
3317
+ const rangeDays = Number(action.rangeDays);
3318
+ if (!BUDGET_SERIES_RANGES.includes(rangeDays) || rangeDays === state.budget.rangeDays) return state;
3319
+ return {
3320
+ ...state,
3321
+ budget: {
3322
+ ...state.budget,
3323
+ rangeDays,
3324
+ series: {
3325
+ provider: null, scope: "*", rangeDays, days: [],
3326
+ loading: false, loaded: false, error: null, fetchedAt: 0,
3327
+ },
3328
+ systemUsage: {
3329
+ provider: null, scope: "*", rangeDays, totals: null, days: [], breakdown: [],
3330
+ loading: false, loaded: false, error: null, fetchedAt: 0,
3331
+ },
3332
+ },
3333
+ };
3334
+ }
3335
+
3336
+ // Selecting a provider expands its model rows and opens its chart.
3337
+ // Selecting the one already selected clears the selection, which is
3338
+ // how a row collapses again.
3339
+ case "budget/selectProvider": {
3340
+ const provider = normalizeProviderName(action.provider);
3341
+ // '*' is the provider itself; anything else is one of its
3342
+ // model-scoped limits, which is a row you can stand on too — the
3343
+ // chart then shows that model alone.
3344
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3345
+ const was = state.budget.selectedProvider;
3346
+ const wasScope = state.budget.selectedScope || "*";
3347
+ // Clicking the row you are already on clears it: a provider row
3348
+ // collapses, a model row falls back to the provider above it.
3349
+ let selectedProvider = provider;
3350
+ let selectedScope = scope;
3351
+ if (provider === was && scope === wasScope) {
3352
+ if (scope === "*") { selectedProvider = null; selectedScope = "*"; }
3353
+ else { selectedScope = "*"; }
3354
+ }
3355
+ if (selectedProvider === was && selectedScope === wasScope) return state;
3356
+ return {
3357
+ ...state,
3358
+ budget: {
3359
+ ...state.budget,
3360
+ selectedProvider,
3361
+ selectedScope,
3362
+ // Choosing a row that exists answers the question the
3363
+ // missing name raised, so the line about it goes.
3364
+ missingProvider: null,
3365
+ // The old chart described the old row. Leaving it up under
3366
+ // a new name is the one way this screen could show one
3367
+ // provider's spend labelled as another's — or a whole
3368
+ // provider's spend labelled as one model's.
3369
+ series: {
3370
+ provider: null, scope: "*", rangeDays: state.budget.rangeDays ?? BUDGET_SERIES_DAYS, days: [],
3371
+ loading: false, loaded: false, error: null, fetchedAt: 0,
3372
+ },
3373
+ systemUsage: {
3374
+ provider: null, scope: "*", rangeDays: state.budget.rangeDays ?? BUDGET_SERIES_DAYS,
3375
+ totals: null, days: [], breakdown: [],
3376
+ loading: false, loaded: false, error: null, fetchedAt: 0,
3377
+ },
3378
+ },
3379
+ };
3380
+ }
3381
+
3382
+ case "budget/series/loading": {
3383
+ const provider = normalizeProviderName(action.provider);
3384
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3385
+ const rangeDays = Number(action.rangeDays) || state.budget.rangeDays || BUDGET_SERIES_DAYS;
3386
+ return {
3387
+ ...state,
3388
+ budget: {
3389
+ ...state.budget,
3390
+ series: { ...state.budget.series, provider, scope, rangeDays, loading: true, error: null },
3391
+ },
3392
+ };
3393
+ }
3394
+
3395
+ // A response for a provider the reader has already left is dropped.
3396
+ // The table and the chart under it must always name the same provider.
3397
+ case "budget/series/loaded": {
3398
+ const provider = normalizeProviderName(action.provider);
3399
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3400
+ const rangeDays = action.rangeDays === undefined
3401
+ ? (state.budget.rangeDays || BUDGET_SERIES_DAYS)
3402
+ : Number(action.rangeDays);
3403
+ if (provider !== state.budget.selectedProvider) return state;
3404
+ // The SCOPE has to match too, or a provider-wide answer that was
3405
+ // already in flight paints itself under a model row's heading.
3406
+ if (scope !== (state.budget.selectedScope || "*")) return state;
3407
+ if (rangeDays !== (state.budget.rangeDays || BUDGET_SERIES_DAYS)) return state;
3408
+ return {
3409
+ ...state,
3410
+ budget: {
3411
+ ...state.budget,
3412
+ series: {
3413
+ provider,
3414
+ scope,
3415
+ rangeDays,
3416
+ days: Array.isArray(action.days) ? action.days : [],
3417
+ loading: false,
3418
+ loaded: true,
3419
+ error: null,
3420
+ fetchedAt: Date.now(),
3421
+ },
3422
+ },
3423
+ };
3424
+ }
3425
+
3426
+ case "budget/series/failed": {
3427
+ const provider = normalizeProviderName(action.provider);
3428
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3429
+ const rangeDays = action.rangeDays === undefined
3430
+ ? (state.budget.rangeDays || BUDGET_SERIES_DAYS)
3431
+ : Number(action.rangeDays);
3432
+ if (provider !== state.budget.selectedProvider) return state;
3433
+ if (scope !== (state.budget.selectedScope || "*")) return state;
3434
+ if (rangeDays !== (state.budget.rangeDays || BUDGET_SERIES_DAYS)) return state;
3435
+ const message = action.error ? String(action.error?.message || action.error) : "";
3436
+ return {
3437
+ ...state,
3438
+ budget: {
3439
+ ...state.budget,
3440
+ series: {
3441
+ ...state.budget.series,
3442
+ provider,
3443
+ scope,
3444
+ rangeDays,
3445
+ loading: false,
3446
+ // The numbers already drawn stay put and are marked
3447
+ // old; a failed re-read is not a day with no usage.
3448
+ error: message || "Could not read the daily usage",
3449
+ },
3450
+ },
3451
+ };
3452
+ }
3453
+
3454
+ case "budget/systemUsage/loading": {
3455
+ const provider = normalizeProviderName(action.provider);
3456
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3457
+ const rangeDays = Number(action.rangeDays) || state.budget.rangeDays || BUDGET_SERIES_DAYS;
3458
+ return {
3459
+ ...state,
3460
+ budget: {
3461
+ ...state.budget,
3462
+ systemUsage: { ...state.budget.systemUsage, provider, scope, rangeDays, loading: true, error: null },
3463
+ },
3464
+ };
3465
+ }
3466
+
3467
+ case "budget/systemUsage/loaded": {
3468
+ const provider = normalizeProviderName(action.provider);
3469
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3470
+ const rangeDays = action.rangeDays === undefined
3471
+ ? (state.budget.rangeDays || BUDGET_SERIES_DAYS)
3472
+ : Number(action.rangeDays);
3473
+ if (provider !== state.budget.selectedProvider || scope !== (state.budget.selectedScope || "*")) return state;
3474
+ if (rangeDays !== (state.budget.rangeDays || BUDGET_SERIES_DAYS)) return state;
3475
+ const report = action.report || {};
3476
+ return {
3477
+ ...state,
3478
+ budget: {
3479
+ ...state.budget,
3480
+ systemUsage: {
3481
+ provider,
3482
+ scope,
3483
+ rangeDays,
3484
+ totals: report.totals || { tokensTotal: 0, turns: 0, sessions: 0 },
3485
+ days: Array.isArray(report.daily) ? report.daily : [],
3486
+ breakdown: Array.isArray(report.breakdown) ? report.breakdown : [],
3487
+ loading: false,
3488
+ loaded: true,
3489
+ error: null,
3490
+ fetchedAt: Date.now(),
3491
+ },
3492
+ },
3493
+ };
3494
+ }
3495
+
3496
+ case "budget/systemUsage/failed": {
3497
+ const provider = normalizeProviderName(action.provider);
3498
+ const scope = typeof action.scope === "string" && action.scope ? action.scope : "*";
3499
+ const rangeDays = action.rangeDays === undefined
3500
+ ? (state.budget.rangeDays || BUDGET_SERIES_DAYS)
3501
+ : Number(action.rangeDays);
3502
+ if (provider !== state.budget.selectedProvider || scope !== (state.budget.selectedScope || "*")) return state;
3503
+ if (rangeDays !== (state.budget.rangeDays || BUDGET_SERIES_DAYS)) return state;
3504
+ const message = action.error ? String(action.error?.message || action.error) : "";
3505
+ return {
3506
+ ...state,
3507
+ budget: {
3508
+ ...state.budget,
3509
+ systemUsage: {
3510
+ ...state.budget.systemUsage,
3511
+ provider,
3512
+ scope,
3513
+ rangeDays,
3514
+ loading: false,
3515
+ error: message || "Could not read system spend",
3516
+ },
3517
+ },
3518
+ };
3519
+ }
3520
+
2873
3521
  // The agent-driven flip. The guards (active session, freshness,
2874
3522
  // opt-out) live in the controller; by the time this dispatches the
2875
3523
  // flip is decided. flipSeq ticks even when the mode is already