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.
- package/mcp/README.md +25 -1
- package/mcp/dist/src/auth.d.ts.map +1 -1
- package/mcp/dist/src/auth.js +5 -3
- package/mcp/dist/src/auth.js.map +1 -1
- package/mcp/dist/src/resources/models.d.ts.map +1 -1
- package/mcp/dist/src/resources/models.js +21 -14
- package/mcp/dist/src/resources/models.js.map +1 -1
- package/mcp/dist/src/server.d.ts.map +1 -1
- package/mcp/dist/src/server.js +2 -0
- package/mcp/dist/src/server.js.map +1 -1
- package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -1
- package/mcp/dist/src/tools/agent-packages.js +28 -16
- package/mcp/dist/src/tools/agent-packages.js.map +1 -1
- package/mcp/dist/src/tools/models.d.ts.map +1 -1
- package/mcp/dist/src/tools/models.js +55 -47
- package/mcp/dist/src/tools/models.js.map +1 -1
- package/mcp/dist/src/tools/providers.d.ts +18 -0
- package/mcp/dist/src/tools/providers.d.ts.map +1 -0
- package/mcp/dist/src/tools/providers.js +284 -0
- package/mcp/dist/src/tools/providers.js.map +1 -0
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
- package/mcp/dist/src/tools/sessions.js +10 -1
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/mcp/dist/src/tools/system.js +2 -2
- package/mcp/dist/src/tools/system.js.map +1 -1
- package/mcp/dist/src/util/respond.d.ts.map +1 -1
- package/mcp/dist/src/util/respond.js +6 -0
- package/mcp/dist/src/util/respond.js.map +1 -1
- package/package.json +3 -3
- package/tui/README.md +8 -5
- package/tui/src/agents-cli.js +80 -78
- package/tui/src/app.js +119 -17
- package/tui/src/node-sdk-transport.js +150 -347
- package/ui/core/src/controller.js +1079 -23
- package/ui/core/src/history.js +25 -1
- package/ui/core/src/reducer.js +649 -1
- package/ui/core/src/selectors.js +906 -6
- package/ui/core/src/state.js +163 -3
- package/ui/react/src/components.js +146 -4
- package/ui/react/src/web-app.js +1942 -125
- package/web/README.md +4 -4
- package/web/api/router.js +33 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-kpxu_PEa.js → abnfDiagram-VRR7QNED-BqKIaTz5.js} +1 -1
- package/web/dist/assets/{arc-BnVQmtB5.js → arc-DCSwKNAt.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CDynQ_6_.js → architectureDiagram-ZJ3FMSHR-B6j7Bv5B.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-BUpYaSDb.js → blockDiagram-677ZJIJ3-BT1iegLO.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-ZL8eskDB.js → c4Diagram-LMCZKHZV-DXEt-_ny.js} +1 -1
- package/web/dist/assets/channel-BLVoqfUa.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-o0Nsyp1T.js → chunk-2Q5K7J3B-B144-8I2.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-CH5CSNRP.js → chunk-32BRIVSS-CMHrtYOC.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-C9wwtPWL.js → chunk-5VM5RSS4-4r8OmbGE.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG--F9yFbhA.js → chunk-EX3LRPZG-R_j3wosN.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BPIstyWL.js → chunk-JWPE2WC7-BKlFEYXC.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-5Dm2u-0k.js → chunk-MOJQB5TN-C2S4TPSa.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-Dbtf9iQK.js → chunk-RYQCIY6F-Y2kpzPD7.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DrZEBJ5l.js → chunk-V7JOEXUC-kAOP_3Kc.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-AqAUynqK.js → chunk-VR4S4FIN-B2Au1EfR.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DQlFaqCy.js → chunk-XXDRQBXY-CZfTG6DD.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-BgaYclQI.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-BgaYclQI.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-RAtPG1Ou.js → cose-bilkent-JH36ORCC--tNLpMEb.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CbeHaUWZ.js → cynefin-VYW2F7L2-BeIRFyvX.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-B6paqF5Y.js → cynefinDiagram-TSTJHNR4-Bm5lb_xz.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DziTBO2K.js → dagre-VKFMJZFB-Cz1i_B0y.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-CPRUiLcr.js → diagram-FQU43EPY-BF0j7c1T.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CIMVdcL1.js → diagram-G47NLZAW-C2QWJ-Y4.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-CHPFllxR.js → diagram-NH7WQ7WH-BiNeThAA.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DwS6unBJ.js → diagram-OA4YK3LP-Cq7_37rx.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-DKWgkObI.js → diagram-WEI45ONY-Cl0GjIma.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-zAVWV6-D.js → ebnfDiagram-CCIWWBDH-C-dTpVrf.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-D0tYX4h0.js → erDiagram-Q63AITRT-RPSg6Lws.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-Cfq4I7R0.js → flowDiagram-23GEKE2U-CslnZm0D.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-wgBAHr5I.js → ganttDiagram-NO4QXBWP-dzr6nimS.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BTIPX5PR.js → gitGraphDiagram-IHSO6WYX-UpjDHGP0.js} +1 -1
- package/web/dist/assets/index-C-ujPKpy.css +1 -0
- package/web/dist/assets/index-Cf_e477H.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CRLiQ87D.js → infoDiagram-FWYZ7A6U-R4qLdwe7.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CwzwUis1.js → ishikawaDiagram-FXEZZL3T-Cst7PjW4.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B9pqMeFw.js → journeyDiagram-5HDEW3XC-CugDTBMp.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CZ5Xky4p.js → kanban-definition-HUTT4EX6-D6Yf2Lgg.js} +1 -1
- package/web/dist/assets/{linear-BLZ2eH4a.js → linear-hUntimCu.js} +1 -1
- package/web/dist/assets/{mermaid.core-CPN1IVjz.js → mermaid.core-BhKlcGBI.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-MdC8d2tc.js → mindmap-definition-LN4V7U3C-DKVBRUj4.js} +1 -1
- package/web/dist/assets/{papaparse.min-CtrVJupf.js → papaparse.min-DbTWBfDP.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-B2DqAc-A.js → pegDiagram-2B236MQR-DeilQyRr.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-C3QAoXZh.js → pieDiagram-ENE6RG2P-CSVE3nUJ.js} +1 -1
- package/web/dist/assets/pilotswarm-DaQpAPI0.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BoUr5qFH.js → quadrantDiagram-ABIIQ3AL-Di-WUa_j.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-bZzE4KFN.js → railroadDiagram-RFXS5EU6-jBlJSP-m.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-B2ENAqLS.js → requirementDiagram-TGXJPOKE-BlLuScOr.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-AJZ3h46Z.js → sankeyDiagram-HTMAVEWB-D76NurlB.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-QUf0VVE7.js → sequenceDiagram-DBY2YBRQ-BGPIUJo0.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-Cv9RcUy6.js → sizeCapture-X5ZJPWSS-DKmGUrSI.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-hDiye45t.js → stateDiagram-2N3HPSRC-DAYHO_nW.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-jZoHf_Ku.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-hSk_OF85.js → swimlanes-5IMT3BWC-CiRowk4M.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-C0ms7UKB.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-ccOZEJ5e.js → timeline-definition-FHXFAJF6-iN7kQL24.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-Bk5DLH_V.js → vennDiagram-L72KCM5P-C6TtJ36b.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-C20AX8ot.js → wardleyDiagram-EHGQE667-CBiv7EU-.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DaHqmi-R.js → xychartDiagram-FW5EYKEG-zXUejHih.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +220 -2
- package/web/server.js +35 -7
- package/web/dist/assets/channel-Bi5A6sHR.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-D8dCxI54.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-D8dCxI54.js +0 -1
- package/web/dist/assets/index-OHj-gGHp.js +0 -24
- package/web/dist/assets/index-eTH1FBtN.css +0 -1
- package/web/dist/assets/pilotswarm-CF1nmpMy.js +0 -97
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Bu4ufvaU.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTFePb1Y.js +0 -8
package/ui/core/src/reducer.js
CHANGED
|
@@ -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
|