pilotswarm 0.5.47 → 0.5.49
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/dist/src/tools/providers.d.ts.map +1 -1
- package/mcp/dist/src/tools/providers.js +12 -0
- package/mcp/dist/src/tools/providers.js.map +1 -1
- package/package.json +2 -2
- package/tui/src/node-sdk-transport.js +5 -0
- package/ui/core/src/commands.js +1 -0
- package/ui/core/src/controller.js +85 -3
- package/ui/core/src/history.js +5 -37
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/reducer.js +42 -0
- package/ui/core/src/selectors.js +153 -4
- package/ui/core/src/state.js +20 -0
- package/ui/core/src/table-layout.js +155 -0
- package/ui/react/src/web-app.js +479 -249
- package/web/dist/assets/{abnfDiagram-VRR7QNED-VPL8m_9W.js → abnfDiagram-VRR7QNED-CXF6f3n3.js} +1 -1
- package/web/dist/assets/{arc-T5I3-c1B.js → arc-MlDVINID.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DEP70bg6.js → architectureDiagram-ZJ3FMSHR-Bdz1J9H-.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-CvBu1MbO.js → blockDiagram-677ZJIJ3-tO706-rr.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-B5qwF4gm.js → c4Diagram-LMCZKHZV-BuZrMrYZ.js} +1 -1
- package/web/dist/assets/channel-moQWQGwr.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-zVW8dS7X.js → chunk-2Q5K7J3B-BWWIPnCk.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-DWctxZZS.js → chunk-32BRIVSS-TyL8x2Q5.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-Bq7_Vmf9.js → chunk-5VM5RSS4-ZTwZyN5B.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-B8CFB3v-.js → chunk-EX3LRPZG-CjWsKz70.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-B7gLPjgZ.js → chunk-JWPE2WC7-BNQrzsa3.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-B2x6q12r.js → chunk-MOJQB5TN-B51xJi0l.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-XorU9Stx.js → chunk-RYQCIY6F-BCdsh9nc.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-CsLa6pw5.js → chunk-V7JOEXUC-C7FFxC7C.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-BBv9mwmj.js → chunk-VR4S4FIN-DWoFV0Pb.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-Cjm4mm6l.js → chunk-XXDRQBXY-plM48yQb.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-33Sv-tmv.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-33Sv-tmv.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Zw9ZQbVl.js → cose-bilkent-JH36ORCC-cPPsd8jq.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-BYnR9YLM.js → cynefin-VYW2F7L2-DLoMRcB-.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Cx8L0dOv.js → cynefinDiagram-TSTJHNR4-DbxB1EcR.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-7GaqVcqz.js → dagre-VKFMJZFB-Cl5J3v7U.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BWSRPJvU.js → diagram-FQU43EPY-C_5l69HJ.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-BELuAE1e.js → diagram-G47NLZAW-B_nxM69x.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-DGXhPpri.js → diagram-NH7WQ7WH-BogLLCcM.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DV8HqN0T.js → diagram-OA4YK3LP-1HZ1DJgO.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Clv1X3ZL.js → diagram-WEI45ONY-CZ9akwtz.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-TpQx36oi.js → ebnfDiagram-CCIWWBDH-CCg2iaF2.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DVVWd00c.js → erDiagram-Q63AITRT-BCYw6hLk.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-Cdz_8_Dw.js → flowDiagram-23GEKE2U-B3O-UeAj.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-zlco-o7u.js → ganttDiagram-NO4QXBWP-GJzz1tR4.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CfAZW1En.js → gitGraphDiagram-IHSO6WYX-Ch1cfaOe.js} +1 -1
- package/web/dist/assets/index-BUp5hFdX.css +1 -0
- package/web/dist/assets/{index-tQXCQojP.js → index-wYKm3Bnl.js} +6 -6
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-0R-8efwI.js → infoDiagram-FWYZ7A6U-Bjo2ImAx.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-lPpoXGlp.js → ishikawaDiagram-FXEZZL3T-Cknf936W.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-D05vSVqu.js → journeyDiagram-5HDEW3XC-Chy2vDKP.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-ClaDdESv.js → kanban-definition-HUTT4EX6-BL4WMBwi.js} +1 -1
- package/web/dist/assets/{linear-CUnhHMvY.js → linear-B8XSGnZ8.js} +1 -1
- package/web/dist/assets/{mermaid.core-DaYS-X8Y.js → mermaid.core-CFdkmvEY.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-N480VHds.js → mindmap-definition-LN4V7U3C-CHOBIkSR.js} +1 -1
- package/web/dist/assets/{papaparse.min-CNTcNgd8.js → papaparse.min-DnG1sqyK.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-CVWyJ5ck.js → pegDiagram-2B236MQR-VuMpZEdm.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-UR9uohPf.js → pieDiagram-ENE6RG2P-CkFTLWPp.js} +1 -1
- package/web/dist/assets/pilotswarm-CLJw14xm.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-D2SIOetn.js → quadrantDiagram-ABIIQ3AL-B74MHjsI.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-OgN6qgOU.js → railroadDiagram-RFXS5EU6-CwcT-BI1.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BSUURBBz.js → requirementDiagram-TGXJPOKE-CfV8rmaQ.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BBZhmSBZ.js → sankeyDiagram-HTMAVEWB-BECA4LLx.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-C10IDNdW.js → sequenceDiagram-DBY2YBRQ-C80kbBkw.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-B9787MHa.js → sizeCapture-X5ZJPWSS-BgdLZC94.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BmSBo7es.js → stateDiagram-2N3HPSRC-DY_mnuOT.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Dw3VhZ5V.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-DOXpv_KQ.js → swimlanes-5IMT3BWC-9m8kjDvw.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DoOI0AiK.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-SjhGfPdI.js → timeline-definition-FHXFAJF6-CRDqu4Os.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BiujYen6.js → vennDiagram-L72KCM5P-B2KNeFvN.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-6nLXBvwc.js → wardleyDiagram-EHGQE667-DC0vN3Ub.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Di7ioTvW.js → xychartDiagram-FW5EYKEG-DOY6Rwu4.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +10 -0
- package/web/dist/assets/channel-D5LojeMw.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C5edc5Or.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C5edc5Or.js +0 -1
- package/web/dist/assets/index-CMtzFJre.css +0 -1
- package/web/dist/assets/pilotswarm-BG6hk0oE.js +0 -97
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BqpsfoJI.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-k8WG_bWt.js +0 -8
package/ui/react/src/web-app.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
selectModelPickerModal,
|
|
48
48
|
selectNavigationError,
|
|
49
49
|
selectProviderTable,
|
|
50
|
+
selectUsageSummary,
|
|
50
51
|
selectReasoningEffortPickerModal,
|
|
51
52
|
selectContextTierPickerModal,
|
|
52
53
|
selectRenameSessionModal,
|
|
@@ -63,6 +64,8 @@ import {
|
|
|
63
64
|
normalizeStoredLayoutAdjustments,
|
|
64
65
|
normalizeStoredPinnedSessionIds,
|
|
65
66
|
normalizeStoredSessionOrder,
|
|
67
|
+
computeFitWidthColumnLayout,
|
|
68
|
+
normalizeTableCellText,
|
|
66
69
|
} from "pilotswarm/ui-core";
|
|
67
70
|
import { useControllerSelector } from "./use-controller-state.js";
|
|
68
71
|
|
|
@@ -643,31 +646,6 @@ const PORTAL_COLUMN_RESIZER_PX = 16;
|
|
|
643
646
|
*/
|
|
644
647
|
const PORTAL_CANVAS_MIN_PX = 480;
|
|
645
648
|
|
|
646
|
-
/**
|
|
647
|
-
* Wide enough to OPEN the canvas by default on a first visit.
|
|
648
|
-
*
|
|
649
|
-
* Sessions and chat need roughly 224px + 500px to stay comfortable, and the
|
|
650
|
-
* canvas wants PORTAL_CANVAS_MIN_PX beside them. 1440 clears that with room to
|
|
651
|
-
* spare and is a real device width (a 14" MacBook, a 900p desktop), so the
|
|
652
|
-
* common laptop gets a canvas and a small window does not get three cramped
|
|
653
|
-
* columns.
|
|
654
|
-
*/
|
|
655
|
-
const PORTAL_CANVAS_DEFAULT_MIN_VIEWPORT_PX = 1440;
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Whether this profile has ever expressed a preference about the two columns.
|
|
659
|
-
*
|
|
660
|
-
* The width-based default must fire only in its absence: someone who CLOSED
|
|
661
|
-
* the canvas has stored `{canvasOpen: false}`, which is a choice and reads
|
|
662
|
-
* nothing like "unset". A legacy rightPaneMode counts too — it is the same
|
|
663
|
-
* preference in the older shape.
|
|
664
|
-
*/
|
|
665
|
-
function hasStoredDesktopPanes(settings) {
|
|
666
|
-
if (!settings || typeof settings !== "object") return false;
|
|
667
|
-
if (settings.desktopPanes && typeof settings.desktopPanes === "object") return true;
|
|
668
|
-
return settings.rightPaneMode === "canvas" || settings.rightPaneMode === "panes";
|
|
669
|
-
}
|
|
670
|
-
|
|
671
649
|
/**
|
|
672
650
|
* The widths the two optional columns OPEN at, in pixels. 2:1 — the canvas is
|
|
673
651
|
* the thing being looked at; diagnostics is a readout beside it.
|
|
@@ -2254,139 +2232,6 @@ function renderInlineMarkdown(source, theme, keyPrefix = "md") {
|
|
|
2254
2232
|
});
|
|
2255
2233
|
}
|
|
2256
2234
|
|
|
2257
|
-
function normalizeTableCellText(value = "") {
|
|
2258
|
-
return String(value || "")
|
|
2259
|
-
.replace(/\s+/g, " ")
|
|
2260
|
-
.trim();
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
const FIT_WIDTH_FLEX_HEADER_KEYWORDS = [
|
|
2264
|
-
"description",
|
|
2265
|
-
"mechanism",
|
|
2266
|
-
"details",
|
|
2267
|
-
"notes",
|
|
2268
|
-
"summary",
|
|
2269
|
-
"message",
|
|
2270
|
-
"comment",
|
|
2271
|
-
"reason",
|
|
2272
|
-
"body",
|
|
2273
|
-
"content",
|
|
2274
|
-
"explanation",
|
|
2275
|
-
"rationale",
|
|
2276
|
-
"one-liner",
|
|
2277
|
-
];
|
|
2278
|
-
|
|
2279
|
-
const FIT_WIDTH_FLEX_MIN_MAX_LEN = 24;
|
|
2280
|
-
const FIT_WIDTH_RIGID_CHAR_CAP = 32;
|
|
2281
|
-
const FIT_WIDTH_MIN_RIGID_CHARS = 6;
|
|
2282
|
-
const FIT_WIDTH_FLEX_MIN_CHARS = 30;
|
|
2283
|
-
const FIT_WIDTH_FLEX_MAX_CHARS = 56;
|
|
2284
|
-
const FIT_WIDTH_FLEX_MIN_FRACTION = 0.4;
|
|
2285
|
-
const FIT_WIDTH_FLEX_TO_RIGID_RATIO = 1.4;
|
|
2286
|
-
const FIT_WIDTH_MIN_EXTRA_CHARS = 2;
|
|
2287
|
-
|
|
2288
|
-
/**
|
|
2289
|
-
* Compute per-column layout for a fit-width markdown / chat table.
|
|
2290
|
-
*
|
|
2291
|
-
* Strategy:
|
|
2292
|
-
* 1. Measure max cell length and "wrappability" (cells with whitespace) per column.
|
|
2293
|
-
* 2. Identify a single "flex" column — long, prose-like, ideally with a
|
|
2294
|
-
* header keyword like Description / Mechanism / Notes. This column
|
|
2295
|
-
* absorbs overflow by wrapping aggressively.
|
|
2296
|
-
* 3. Give every other column a budget = clamp(maxLen + padding, MIN, RIGID_CAP),
|
|
2297
|
-
* so rigid columns stay at their content-fit width even when one
|
|
2298
|
-
* sibling column has hundreds of characters of prose.
|
|
2299
|
-
* 4. Give the flex column a bounded share of the rigid-column budget. A
|
|
2300
|
-
* very long cell should wrap; it should not steal so much percentage
|
|
2301
|
-
* width that compact columns like Count / Status collapse on phones.
|
|
2302
|
-
* 5. Return a minimum table width in ch so the wrapper can scroll
|
|
2303
|
-
* horizontally rather than forcing all columns below readable size.
|
|
2304
|
-
*
|
|
2305
|
-
* Returns { widths: ["12.34%", ...], flexIndex: number, minWidth: "64ch" }
|
|
2306
|
-
* when a flex column is identified — the table renderer then forces
|
|
2307
|
-
* table-layout: fixed, adds an `is-flex-column` class to the chosen column,
|
|
2308
|
-
* and gives the table a readable minimum width. Returns null when no flex
|
|
2309
|
-
* column is found, in which case the renderer falls back to the browser's
|
|
2310
|
-
* auto-table-layout (which is already good for short / uniform tables).
|
|
2311
|
-
*
|
|
2312
|
-
* Background: the previous behavior used the browser's auto-table-layout
|
|
2313
|
-
* unconditionally. That works well when columns are uniform but is biased
|
|
2314
|
-
* toward wide columns when one column has prose hundreds of characters
|
|
2315
|
-
* long (e.g. a Mechanism / Description column) — auto-layout distributes
|
|
2316
|
-
* width proportional to (max-content − min-content), which lets the prose
|
|
2317
|
-
* column squeeze the rigid columns down to a few characters each.
|
|
2318
|
-
*/
|
|
2319
|
-
function computeFitWidthColumnLayout(rows = []) {
|
|
2320
|
-
const columnCount = Math.max(0, ...rows.map((row) => row.length));
|
|
2321
|
-
if (columnCount <= 0) return null;
|
|
2322
|
-
|
|
2323
|
-
const headerRow = rows[0] || [];
|
|
2324
|
-
const dataRowCount = Math.max(1, rows.length - 1);
|
|
2325
|
-
|
|
2326
|
-
const stats = Array.from({ length: columnCount }, () => ({
|
|
2327
|
-
max: 0,
|
|
2328
|
-
spaceCells: 0,
|
|
2329
|
-
}));
|
|
2330
|
-
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
2331
|
-
const row = rows[rowIndex];
|
|
2332
|
-
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
2333
|
-
const text = normalizeTableCellText(row[columnIndex] || "");
|
|
2334
|
-
if (text.length > stats[columnIndex].max) stats[columnIndex].max = text.length;
|
|
2335
|
-
if (rowIndex > 0 && /\s/.test(text)) stats[columnIndex].spaceCells += 1;
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
let flexIndex = -1;
|
|
2340
|
-
let flexScore = 0;
|
|
2341
|
-
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
2342
|
-
const stat = stats[columnIndex];
|
|
2343
|
-
if (stat.max < FIT_WIDTH_FLEX_MIN_MAX_LEN) continue;
|
|
2344
|
-
const spaceRatio = stat.spaceCells / dataRowCount;
|
|
2345
|
-
if (spaceRatio < 0.4) continue;
|
|
2346
|
-
const headerText = normalizeTableCellText(headerRow[columnIndex] || "").toLowerCase();
|
|
2347
|
-
const headerBonus = FIT_WIDTH_FLEX_HEADER_KEYWORDS.some((keyword) => headerText.includes(keyword)) ? 60 : 0;
|
|
2348
|
-
const score = stat.max + headerBonus;
|
|
2349
|
-
if (score > flexScore) {
|
|
2350
|
-
flexScore = score;
|
|
2351
|
-
flexIndex = columnIndex;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
if (flexIndex < 0) return null;
|
|
2356
|
-
|
|
2357
|
-
const rigidBudgets = stats.map((stat, columnIndex) => {
|
|
2358
|
-
if (columnIndex === flexIndex) return 0;
|
|
2359
|
-
return Math.max(FIT_WIDTH_MIN_RIGID_CHARS, Math.min(stat.max + 2, FIT_WIDTH_RIGID_CHAR_CAP));
|
|
2360
|
-
});
|
|
2361
|
-
const sumRigid = rigidBudgets.reduce((sum, value) => sum + value, 0);
|
|
2362
|
-
const flexStat = stats[flexIndex];
|
|
2363
|
-
const flexBudget = Math.max(
|
|
2364
|
-
FIT_WIDTH_FLEX_MIN_CHARS,
|
|
2365
|
-
Math.min(
|
|
2366
|
-
flexStat.max * 0.45,
|
|
2367
|
-
FIT_WIDTH_FLEX_MAX_CHARS,
|
|
2368
|
-
Math.max(FIT_WIDTH_FLEX_MIN_CHARS, sumRigid * FIT_WIDTH_FLEX_TO_RIGID_RATIO),
|
|
2369
|
-
),
|
|
2370
|
-
);
|
|
2371
|
-
const budgets = rigidBudgets.map((value, columnIndex) => (
|
|
2372
|
-
columnIndex === flexIndex ? flexBudget : value
|
|
2373
|
-
));
|
|
2374
|
-
|
|
2375
|
-
const totalBudget = budgets.reduce((sum, value) => sum + value, 0);
|
|
2376
|
-
if (totalBudget > 0 && budgets[flexIndex] / totalBudget < FIT_WIDTH_FLEX_MIN_FRACTION) {
|
|
2377
|
-
const sumRigid = totalBudget - budgets[flexIndex];
|
|
2378
|
-
budgets[flexIndex] = sumRigid * (FIT_WIDTH_FLEX_MIN_FRACTION / (1 - FIT_WIDTH_FLEX_MIN_FRACTION));
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
const finalTotal = budgets.reduce((sum, value) => sum + value, 0);
|
|
2382
|
-
if (!(finalTotal > 0)) return null;
|
|
2383
|
-
return {
|
|
2384
|
-
widths: budgets.map((value) => `${((value / finalTotal) * 100).toFixed(2)}%`),
|
|
2385
|
-
flexIndex,
|
|
2386
|
-
minWidth: `${Math.ceil(finalTotal + FIT_WIDTH_MIN_EXTRA_CHARS)}ch`,
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
2235
|
function buildTableColumnLabels(headerRows = [], columnCount = 0) {
|
|
2391
2236
|
return Array.from({ length: columnCount }, (_, columnIndex) => {
|
|
2392
2237
|
const label = (Array.isArray(headerRows) ? headerRows : [])
|
|
@@ -9137,6 +8982,17 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9137
8982
|
onClick: () => controller.handleCommand(adminVisible ? UI_COMMANDS.CLOSE_ADMIN_CONSOLE : UI_COMMANDS.OPEN_ADMIN_CONSOLE).catch(() => {}),
|
|
9138
8983
|
active: adminVisible,
|
|
9139
8984
|
}]),
|
|
8985
|
+
// The Workspace MODE button. Budget and the Admin Console replace the
|
|
8986
|
+
// workspace; this is the way back, as a destination of its own rather
|
|
8987
|
+
// than "close whatever is open". Desktop only — the phone has no
|
|
8988
|
+
// admin console and its view cycle already sits in the header.
|
|
8989
|
+
...(mobile ? [] : [{
|
|
8990
|
+
key: "workspace",
|
|
8991
|
+
icon: React.createElement(MainLayoutGlyph),
|
|
8992
|
+
label: "Workspace — sessions, chat and panels",
|
|
8993
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_WORKSPACE).catch(() => {}),
|
|
8994
|
+
active: !adminVisible && !budgetOpen,
|
|
8995
|
+
}]),
|
|
9140
8996
|
];
|
|
9141
8997
|
|
|
9142
8998
|
const renderButton = (def) => {
|
|
@@ -9206,17 +9062,27 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9206
9062
|
);
|
|
9207
9063
|
}
|
|
9208
9064
|
|
|
9209
|
-
// Two
|
|
9210
|
-
//
|
|
9211
|
-
// the
|
|
9212
|
-
|
|
9065
|
+
// Two kinds of button, two clusters (desktop only):
|
|
9066
|
+
//
|
|
9067
|
+
// left : the workspace's — new, filter, canvas, diagnostics
|
|
9068
|
+
// right : modes [workspace, budget, admin] │ theme · sign-out
|
|
9069
|
+
//
|
|
9070
|
+
// The whole left cluster belongs to the workspace — new session, filter,
|
|
9071
|
+
// canvas and diagnostics all act on it — so it exists only in Workspace
|
|
9072
|
+
// mode. Budget and the Admin Console replace the workspace; nothing on
|
|
9073
|
+
// the left applies there. Modes are exclusive (the controller closes one
|
|
9074
|
+
// when the other opens) and the Workspace button is the way back.
|
|
9075
|
+
const mode = adminVisible ? "admin" : (budgetOpen ? "budget" : "workspace");
|
|
9076
|
+
const ACTIONS = ["new", "filter"];
|
|
9077
|
+
const PANELS = ["canvas", "diagnostics"];
|
|
9078
|
+
const MODES = ["workspace", "budget", "admin"];
|
|
9213
9079
|
|
|
9214
9080
|
// While the canvas is full screen, ANY other button first drops full
|
|
9215
9081
|
// screen and then does its own job. Pressing Filter and watching nothing
|
|
9216
9082
|
// happen behind a canvas — or watching a panel open where you cannot see
|
|
9217
9083
|
// it — is the confusing half of every full-screen mode. The Canvas toggle
|
|
9218
|
-
// is exempt: it already means "put the canvas away", and closing drops
|
|
9219
|
-
// flag in the reducer.
|
|
9084
|
+
// is exempt: it already means "put the canvas away", and closing drops
|
|
9085
|
+
// the flag in the reducer.
|
|
9220
9086
|
const withRestore = (def) => (!canvasMaximized || def.key === "canvas" ? def : {
|
|
9221
9087
|
...def,
|
|
9222
9088
|
onClick: (...args) => {
|
|
@@ -9224,21 +9090,35 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9224
9090
|
return def.onClick?.(...args);
|
|
9225
9091
|
},
|
|
9226
9092
|
});
|
|
9093
|
+
const pick = (keys) => keys
|
|
9094
|
+
.map((key) => buttonDefs.find((d) => d.key === key))
|
|
9095
|
+
.filter(Boolean)
|
|
9096
|
+
.map(withRestore)
|
|
9097
|
+
.map(renderButton);
|
|
9098
|
+
const divider = (key) => React.createElement("span", { key, className: "ps-toolbar-divider", "aria-hidden": "true" });
|
|
9099
|
+
|
|
9100
|
+
// One run, no bar: all four act on the workspace.
|
|
9101
|
+
const leftCluster = mode === "workspace"
|
|
9102
|
+
? [...pick(ACTIONS), ...pick(PANELS)]
|
|
9103
|
+
: [];
|
|
9104
|
+
const rightCluster = [
|
|
9105
|
+
...pick(MODES),
|
|
9106
|
+
divider("mode-divider"),
|
|
9107
|
+
...pick(["theme"]),
|
|
9108
|
+
];
|
|
9227
9109
|
|
|
9228
|
-
const leftButtons = buttonDefs.filter((d) => !RIGHT_GROUP.has(d.key)).map(withRestore);
|
|
9229
|
-
const rightButtons = buttonDefs.filter((d) => RIGHT_GROUP.has(d.key)).map(withRestore);
|
|
9230
9110
|
const toolbar = React.createElement("div", { className: "ps-toolbar" },
|
|
9231
9111
|
// Three columns: left rail | main controls | right rail. Full-screen
|
|
9232
9112
|
// canvas uses the otherwise-empty left rail for the normal actions so
|
|
9233
9113
|
// they do not crowd the canvas metadata and controls across the top.
|
|
9234
9114
|
React.createElement("div", { className: "ps-toolbar-side is-left" },
|
|
9235
|
-
canvasMaximized ?
|
|
9115
|
+
canvasMaximized ? leftCluster : null),
|
|
9236
9116
|
React.createElement("div", { className: "ps-toolbar-actions" },
|
|
9237
|
-
canvasMaximized ? null :
|
|
9117
|
+
canvasMaximized ? null : leftCluster),
|
|
9238
9118
|
React.createElement("div", { className: "ps-toolbar-side is-right" },
|
|
9239
9119
|
// The portal header parks its version/status meta here, LEFT of
|
|
9240
9120
|
// the tool buttons, and its sign-out glyph in the slot after
|
|
9241
|
-
// them — one right-aligned cluster: meta ·
|
|
9121
|
+
// them — one right-aligned cluster: meta · mode · theme · out.
|
|
9242
9122
|
React.createElement("span", { className: "ps-toolbar-meta-slot", id: "ps-toolbar-meta-slot" }),
|
|
9243
9123
|
// While the canvas is full screen its header controls (rev, zoom,
|
|
9244
9124
|
// zen, restore) portal INTO this slot from CanvasPane — zoom state
|
|
@@ -9250,9 +9130,7 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9250
9130
|
canvasMaximized
|
|
9251
9131
|
? React.createElement("span", { className: "ps-toolbar-divider", "aria-hidden": "true" })
|
|
9252
9132
|
: null,
|
|
9253
|
-
|
|
9254
|
-
? React.createElement("div", { className: "ps-toolbar-actions is-tools" }, rightButtons.map(renderButton))
|
|
9255
|
-
: null,
|
|
9133
|
+
React.createElement("div", { className: "ps-toolbar-actions is-tools" }, rightCluster),
|
|
9256
9134
|
React.createElement("span", { className: "ps-toolbar-signout-slot", id: "ps-toolbar-signout-slot" })),
|
|
9257
9135
|
);
|
|
9258
9136
|
|
|
@@ -9723,7 +9601,12 @@ function ChatRightResizeHandle({ controller, target, adjust = 0 }) {
|
|
|
9723
9601
|
const st = dragStateRef.current;
|
|
9724
9602
|
if (!st) return;
|
|
9725
9603
|
// Pointer right = chat grows = the neighbour gives up the pixels.
|
|
9726
|
-
|
|
9604
|
+
// Pointer left stops where chat reaches its floor: past that the
|
|
9605
|
+
// grid cannot give the right block more pixels, and an adjust
|
|
9606
|
+
// that keeps climbing anyway only inflates the stored width so
|
|
9607
|
+
// the next drag has to unwind it before anything moves.
|
|
9608
|
+
const next = st.startAdjust - (event.clientX - st.startX);
|
|
9609
|
+
controller.dispatch({ type: actionType, [key]: Math.min(next, st.maxAdjust) });
|
|
9727
9610
|
};
|
|
9728
9611
|
const onUp = () => {
|
|
9729
9612
|
const st = dragStateRef.current;
|
|
@@ -9751,7 +9634,18 @@ function ChatRightResizeHandle({ controller, target, adjust = 0 }) {
|
|
|
9751
9634
|
if (event.button !== 0) return;
|
|
9752
9635
|
event.preventDefault();
|
|
9753
9636
|
capturePointerForDrag(event);
|
|
9754
|
-
|
|
9637
|
+
// How far the right block may still grow: exactly what chat can
|
|
9638
|
+
// give before it reaches its floor. Measured at drag start from
|
|
9639
|
+
// the chat slot (the main grid's last column), the same element
|
|
9640
|
+
// the grid template protects with its own minimum.
|
|
9641
|
+
const chatSlot = document.querySelector(".ps-workspace-main-grid > .ps-workspace-pane-slot:last-child");
|
|
9642
|
+
const chatWidth = chatSlot ? chatSlot.getBoundingClientRect().width : Number.POSITIVE_INFINITY;
|
|
9643
|
+
const startAdjust = Number(adjust) || 0;
|
|
9644
|
+
dragStateRef.current = {
|
|
9645
|
+
startX: event.clientX,
|
|
9646
|
+
startAdjust,
|
|
9647
|
+
maxAdjust: startAdjust + Math.max(0, chatWidth - PORTAL_MIN_CHAT_COLUMN_PX),
|
|
9648
|
+
};
|
|
9755
9649
|
setDragging(true);
|
|
9756
9650
|
document.body.classList.add("is-resizing-pane-x");
|
|
9757
9651
|
},
|
|
@@ -9788,7 +9682,7 @@ function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
|
9788
9682
|
// real space between the two panes. Its bounds are asymmetric:
|
|
9789
9683
|
// -column/2 makes Inspector 0px; column/2-divider makes Activity
|
|
9790
9684
|
// 0px. Using one symmetric half-range left an 8-9px pane remnant.
|
|
9791
|
-
const next =
|
|
9685
|
+
const next = clampNumber(st.start + (event.clientY - st.startY), st.minAdjust, st.maxAdjust);
|
|
9792
9686
|
controller.dispatch({ type: "ui/diagnosticsSplitAdjust", diagnosticsSplitAdjust: next });
|
|
9793
9687
|
};
|
|
9794
9688
|
const onUp = () => {
|
|
@@ -9821,7 +9715,7 @@ function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
|
9821
9715
|
const { minAdjust, maxAdjust } = getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight);
|
|
9822
9716
|
dragStateRef.current = {
|
|
9823
9717
|
startY: event.clientY,
|
|
9824
|
-
start:
|
|
9718
|
+
start: clampNumber(Number(splitAdjust) || 0, minAdjust, maxAdjust),
|
|
9825
9719
|
minAdjust,
|
|
9826
9720
|
maxAdjust,
|
|
9827
9721
|
};
|
|
@@ -9838,7 +9732,7 @@ function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
|
9838
9732
|
const { minAdjust, maxAdjust } = getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight);
|
|
9839
9733
|
controller.dispatch({
|
|
9840
9734
|
type: "ui/diagnosticsSplitAdjust",
|
|
9841
|
-
diagnosticsSplitAdjust:
|
|
9735
|
+
diagnosticsSplitAdjust: clampNumber(
|
|
9842
9736
|
(Number(splitAdjust) || 0) + (event.key === "ArrowDown" ? dividerHeight : -dividerHeight),
|
|
9843
9737
|
minAdjust,
|
|
9844
9738
|
maxAdjust,
|
|
@@ -10131,7 +10025,25 @@ function budgetCellEl(cell) {
|
|
|
10131
10025
|
}, `/ ${cell.quotaLabel}`));
|
|
10132
10026
|
}
|
|
10133
10027
|
|
|
10134
|
-
/**
|
|
10028
|
+
/**
|
|
10029
|
+
* The four parts of a period cell's used figure, in one cell of the split
|
|
10030
|
+
* row under a SELECTED provider or model row. A limit is on the total, so
|
|
10031
|
+
* the table's cell stays the one number a limit is about; this row says
|
|
10032
|
+
* what went into it, column by column.
|
|
10033
|
+
*/
|
|
10034
|
+
function budgetSplitCellEl(cell) {
|
|
10035
|
+
if (!cell.split) return React.createElement("span", { className: "ps-budget-split" }, "—");
|
|
10036
|
+
// Top row: the two parts that add up to the figure. Bottom row: what
|
|
10037
|
+
// the input was made of — served from the cache, written to it.
|
|
10038
|
+
const cacheTitle = "Parts of the input, not additions to it: the total is input + output.";
|
|
10039
|
+
return React.createElement("span", { className: "ps-budget-split" },
|
|
10040
|
+
React.createElement("span", null, "in ", formatCompactNumber(cell.split.input)),
|
|
10041
|
+
React.createElement("span", null, "out ", formatCompactNumber(cell.split.output)),
|
|
10042
|
+
React.createElement("span", { title: cacheTitle }, "cache r ", formatCompactNumber(cell.split.cacheRead)),
|
|
10043
|
+
React.createElement("span", { title: cacheTitle }, "w ", formatCompactNumber(cell.split.cacheWrite)));
|
|
10044
|
+
}
|
|
10045
|
+
|
|
10046
|
+
/** What a cell says on hover: the period, its reset, whether it is capped, and what the figure is made of. */
|
|
10135
10047
|
function budgetCellTitle(row, column, cell) {
|
|
10136
10048
|
const who = row.kind === "model" ? `${row.providerName} · ${row.label}` : row.providerName;
|
|
10137
10049
|
const cap = cell.uncapped
|
|
@@ -10142,7 +10054,8 @@ function budgetCellTitle(row, column, cell) {
|
|
|
10142
10054
|
const blocked = cell.providerExhausted
|
|
10143
10055
|
? " The provider's own limit for this period is spent, so nothing runs against it whatever your share says."
|
|
10144
10056
|
: "";
|
|
10145
|
-
|
|
10057
|
+
const split = cell.splitText ? ` Made of ${cell.splitText}.` : "";
|
|
10058
|
+
return `${who} · ${column.label} — ${cap}. It ${budgetResetPhrase(cell.resetsLabel)}.${blocked}${split}`;
|
|
10146
10059
|
}
|
|
10147
10060
|
|
|
10148
10061
|
/**
|
|
@@ -10247,6 +10160,19 @@ function ProviderTable({ controller, view, defaults = null }) {
|
|
|
10247
10160
|
title: budgetCellTitle(row, column, row.cells[column.id]),
|
|
10248
10161
|
}, budgetCellEl(row.cells[column.id]))));
|
|
10249
10162
|
|
|
10163
|
+
// Under a selected row: what each of its three figures is made of.
|
|
10164
|
+
const splitRow = (row) => React.createElement("tr", {
|
|
10165
|
+
key: `${row.key}:split`,
|
|
10166
|
+
className: `ps-budget-grid-split${row.kind === "model" ? " is-model" : ""}`,
|
|
10167
|
+
"aria-label": `What ${row.kind === "model" ? row.label : row.providerName} spent, by kind of token`,
|
|
10168
|
+
},
|
|
10169
|
+
// Empty on purpose: the name column keeps the split cells under their
|
|
10170
|
+
// period columns; the numbers explain themselves.
|
|
10171
|
+
React.createElement("td", { className: "ps-budget-grid-split-name" }),
|
|
10172
|
+
view.columns.map((column) => React.createElement("td", {
|
|
10173
|
+
key: column.id, className: "ps-budget-grid-num is-split", "data-period": column.label,
|
|
10174
|
+
}, budgetSplitCellEl(row.cells[column.id]))));
|
|
10175
|
+
|
|
10250
10176
|
return React.createElement(React.Fragment, null,
|
|
10251
10177
|
React.createElement("div", { className: "ps-budget-table-head" },
|
|
10252
10178
|
React.createElement("span", { className: "ps-budget-label" },
|
|
@@ -10279,7 +10205,8 @@ function ProviderTable({ controller, view, defaults = null }) {
|
|
|
10279
10205
|
view.columns.map((column) => React.createElement("th", {
|
|
10280
10206
|
key: column.id, className: "ps-budget-grid-h is-num", scope: "col",
|
|
10281
10207
|
}, column.label)))),
|
|
10282
|
-
|
|
10208
|
+
// A selected row is followed by its split row.
|
|
10209
|
+
React.createElement("tbody", null, view.rows.flatMap((row) => (row.selected ? [bodyRow(row), splitRow(row)] : [bodyRow(row)])))));
|
|
10283
10210
|
}
|
|
10284
10211
|
|
|
10285
10212
|
/**
|
|
@@ -10806,14 +10733,26 @@ function CreateProviderSheet({
|
|
|
10806
10733
|
// Bounded and named here instead, where the rule can be said precisely.
|
|
10807
10734
|
const badChars = trimmed !== "" && !/^[A-Za-z0-9._-]+$/.test(trimmed);
|
|
10808
10735
|
const tooLong = trimmed.length > 64;
|
|
10736
|
+
// A workload-identity type carries no key: the worker authenticates as
|
|
10737
|
+
// itself. So the form asks for nothing to paste, and requiring a
|
|
10738
|
+
// credential would make the type impossible to add from this screen.
|
|
10739
|
+
const workloadIdentity = types.find((option) => option.id === type)?.usesWorkloadIdentity === true;
|
|
10740
|
+
// It runs on the worker's identity, which is the organization's, so it can
|
|
10741
|
+
// only be shared — a personal one would spend the cluster's account under
|
|
10742
|
+
// a name no administrator can cap. The server refuses it too.
|
|
10743
|
+
const sharedOnly = workloadIdentity;
|
|
10744
|
+
const effectiveShared = sharedOnly ? true : shared;
|
|
10809
10745
|
const valid = Boolean(trimmed) && !taken && !badChars && !tooLong
|
|
10810
|
-
&& Boolean(type) && Boolean(credential.trim())
|
|
10746
|
+
&& Boolean(type) && (workloadIdentity || Boolean(credential.trim()))
|
|
10747
|
+
&& (!sharedOnly || isAdmin);
|
|
10811
10748
|
const isGithub = /github/i.test(type);
|
|
10812
10749
|
const submit = () => {
|
|
10813
10750
|
if (!valid) return;
|
|
10814
10751
|
const input = {
|
|
10815
|
-
name: trimmed, type, shared,
|
|
10816
|
-
credentials:
|
|
10752
|
+
name: trimmed, type, shared: effectiveShared,
|
|
10753
|
+
credentials: workloadIdentity
|
|
10754
|
+
? { kind: "workloadIdentity" }
|
|
10755
|
+
: isGithub ? { githubToken: credential.trim() } : { apiKey: credential.trim() },
|
|
10817
10756
|
baseUrl: baseUrl.trim() || null,
|
|
10818
10757
|
};
|
|
10819
10758
|
setCredential("");
|
|
@@ -10826,7 +10765,7 @@ function CreateProviderSheet({
|
|
|
10826
10765
|
|
|
10827
10766
|
return React.createElement(BudgetSheet, {
|
|
10828
10767
|
title: updating ? `Update key for ${trimmed}` : (isGithub ? "Add GitHub Copilot provider" : "Add model provider"),
|
|
10829
|
-
subtitle:
|
|
10768
|
+
subtitle: effectiveShared ? "Shared" : "Personal",
|
|
10830
10769
|
confirmLabel: updating ? "Update key" : "Add provider", busy, disabled: !valid, error, onCancel: cancel, onConfirm: submit,
|
|
10831
10770
|
// The whole point of an update is what does NOT change. Saying so is
|
|
10832
10771
|
// the difference between "replace the key" and "did I just make a
|
|
@@ -10862,33 +10801,45 @@ function CreateProviderSheet({
|
|
|
10862
10801
|
}, types.length === 0
|
|
10863
10802
|
? React.createElement("option", { value: "" }, "No provider types are listed on this deployment")
|
|
10864
10803
|
: types.map((option) => React.createElement("option", { key: option.id, value: option.id }, option.id)))),
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
:
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10804
|
+
workloadIdentity
|
|
10805
|
+
? React.createElement(SheetField, {
|
|
10806
|
+
label: "Authentication",
|
|
10807
|
+
hint: "Nothing is stored. The worker mints a short-lived token for each request.",
|
|
10808
|
+
},
|
|
10809
|
+
React.createElement("div", { className: "ps-budget-input is-static", role: "note" },
|
|
10810
|
+
"Use the Workload Identity configured in the worker"))
|
|
10811
|
+
: React.createElement(SheetField, {
|
|
10812
|
+
label: updating
|
|
10813
|
+
? (isGithub ? "New GitHub Copilot token" : "New API key")
|
|
10814
|
+
: (isGithub ? "GitHub Copilot token" : "API key"),
|
|
10815
|
+
hint: updating
|
|
10816
|
+
? "Replaces the current one. The existing key is never shown."
|
|
10817
|
+
: "Paste the token value.",
|
|
10818
|
+
},
|
|
10819
|
+
React.createElement("input", {
|
|
10820
|
+
className: "ps-budget-input", type: "password", autoComplete: "off",
|
|
10821
|
+
"aria-label": updating
|
|
10822
|
+
? (isGithub ? "New GitHub Copilot token" : "New API key")
|
|
10823
|
+
: (isGithub ? "GitHub Copilot token" : "API key"),
|
|
10824
|
+
value: credential, placeholder: "Paste token",
|
|
10825
|
+
onChange: (event) => { onDirty?.(); setCredential(event.target.value); },
|
|
10826
|
+
// While updating this is the only editable field, and the Name input
|
|
10827
|
+
// that carries the other Enter handler is disabled — so without this
|
|
10828
|
+
// the sheet cannot be submitted from the keyboard at all.
|
|
10829
|
+
onKeyDown: (event) => { if (event.key === "Enter" && valid) submit(); },
|
|
10830
|
+
})),
|
|
10885
10831
|
!updating ? React.createElement(SheetField, { label: "Base URL", hint: "Optional." },
|
|
10886
10832
|
React.createElement("input", {
|
|
10887
10833
|
className: "ps-budget-input", value: baseUrl, placeholder: "https://…",
|
|
10888
10834
|
"aria-label": "Base URL",
|
|
10889
10835
|
onChange: (event) => setBaseUrl(event.target.value),
|
|
10890
10836
|
})) : null,
|
|
10891
|
-
|
|
10837
|
+
sharedOnly && !updating ? React.createElement(SheetField, {
|
|
10838
|
+
label: "Access",
|
|
10839
|
+
hint: "It runs on the worker's own identity, which belongs to the organization, so it cannot be personal.",
|
|
10840
|
+
},
|
|
10841
|
+
React.createElement("div", { className: "ps-budget-input is-static", role: "note" }, "Shared"))
|
|
10842
|
+
: isAdmin && !updating ? React.createElement(SheetField, { label: "Access" },
|
|
10892
10843
|
React.createElement(Segmented, {
|
|
10893
10844
|
value: shared ? "shared" : "mine", onChange: (value) => setShared(value === "shared"),
|
|
10894
10845
|
label: "Access",
|
|
@@ -10941,10 +10892,289 @@ function budgetViewDeps(state) {
|
|
|
10941
10892
|
Boolean(state.ui?.budgetOpen), budget.loading, budget.refreshing, budget.loaded,
|
|
10942
10893
|
budget.error, budget.grid, budget.paused, budget.overall, budget.selectedProvider,
|
|
10943
10894
|
budget.selectedScope, budget.missingProvider, budget.series,
|
|
10895
|
+
budget.tab, budget.summary,
|
|
10944
10896
|
state.auth?.authorization?.role, state.admin?.profile,
|
|
10945
10897
|
];
|
|
10946
10898
|
}
|
|
10947
10899
|
|
|
10900
|
+
// ─── Cluster summary tab ────────────────────────────────────────────────
|
|
10901
|
+
|
|
10902
|
+
const SUMMARY_RANGES = [14, 30, 90];
|
|
10903
|
+
const SUMMARY_PRESETS = [
|
|
10904
|
+
{ id: "all", label: "All providers" },
|
|
10905
|
+
{ id: "shared", label: "Shared providers" },
|
|
10906
|
+
{ id: "users", label: "User providers" },
|
|
10907
|
+
];
|
|
10908
|
+
|
|
10909
|
+
function summaryPresetLabel(view) {
|
|
10910
|
+
if (view.preset === "all") return "All providers";
|
|
10911
|
+
if (view.preset === "shared") return "Shared providers";
|
|
10912
|
+
if (view.preset === "users") return "User providers";
|
|
10913
|
+
const n = view.selectedProviders.length;
|
|
10914
|
+
return n === 0 ? "No providers" : (n === 1 ? view.selectedProviders[0] : `${n} providers`);
|
|
10915
|
+
}
|
|
10916
|
+
|
|
10917
|
+
/**
|
|
10918
|
+
* The provider picker: three presets and a checkbox per provider. A preset
|
|
10919
|
+
* expands to an exact list of names before the request goes out — the
|
|
10920
|
+
* database never has to know what "Shared" meant on the day.
|
|
10921
|
+
*/
|
|
10922
|
+
function SummaryProviderPicker({ view, onChange }) {
|
|
10923
|
+
const [open, setOpen] = React.useState(false);
|
|
10924
|
+
const ref = React.useRef(null);
|
|
10925
|
+
React.useEffect(() => {
|
|
10926
|
+
if (!open) return undefined;
|
|
10927
|
+
const onDown = (event) => { if (ref.current && !ref.current.contains(event.target)) setOpen(false); };
|
|
10928
|
+
const onKey = (event) => { if (event.key === "Escape") setOpen(false); };
|
|
10929
|
+
document.addEventListener("pointerdown", onDown);
|
|
10930
|
+
document.addEventListener("keydown", onKey);
|
|
10931
|
+
return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
|
|
10932
|
+
}, [open]);
|
|
10933
|
+
|
|
10934
|
+
const namesFor = (preset) => (preset === "all" ? [] : view.providers
|
|
10935
|
+
.filter((p) => (preset === "shared" ? p.class === "shared" : p.class !== "shared"))
|
|
10936
|
+
.map((p) => p.name));
|
|
10937
|
+
const checked = new Set(view.preset === "all"
|
|
10938
|
+
? view.providers.map((p) => p.name)
|
|
10939
|
+
: (view.preset === "custom" ? view.selectedProviders : namesFor(view.preset)));
|
|
10940
|
+
|
|
10941
|
+
// A preset is a whole answer, so the menu closes on it; a checkbox is
|
|
10942
|
+
// one step of a hand-picked list, so the menu stays open for the next.
|
|
10943
|
+
const choosePreset = (preset) => { onChange({ preset, providers: namesFor(preset) }); setOpen(false); };
|
|
10944
|
+
const toggle = (name) => {
|
|
10945
|
+
const next = new Set(checked);
|
|
10946
|
+
if (next.has(name)) next.delete(name); else next.add(name);
|
|
10947
|
+
onChange({ preset: "custom", providers: Array.from(next) });
|
|
10948
|
+
};
|
|
10949
|
+
|
|
10950
|
+
return React.createElement("div", { className: "ps-summary-picker", ref },
|
|
10951
|
+
React.createElement("button", {
|
|
10952
|
+
type: "button",
|
|
10953
|
+
className: `ps-mini-button ps-summary-picker__button${open ? " is-open" : ""}`,
|
|
10954
|
+
"aria-haspopup": "listbox",
|
|
10955
|
+
"aria-expanded": open,
|
|
10956
|
+
"aria-label": "Providers filter",
|
|
10957
|
+
onClick: () => setOpen((v) => !v),
|
|
10958
|
+
}, summaryPresetLabel(view), " ", React.createElement("span", { "aria-hidden": "true" }, "▾")),
|
|
10959
|
+
open ? React.createElement("div", { className: "ps-summary-picker__menu", role: "listbox", "aria-label": "Providers" },
|
|
10960
|
+
SUMMARY_PRESETS.map((preset) => React.createElement("button", {
|
|
10961
|
+
key: preset.id,
|
|
10962
|
+
type: "button",
|
|
10963
|
+
role: "option",
|
|
10964
|
+
"aria-selected": view.preset === preset.id,
|
|
10965
|
+
className: `ps-summary-picker__preset${view.preset === preset.id ? " is-on" : ""}`,
|
|
10966
|
+
onClick: () => choosePreset(preset.id),
|
|
10967
|
+
}, preset.label)),
|
|
10968
|
+
React.createElement("div", { className: "ps-summary-picker__rule", "aria-hidden": "true" }),
|
|
10969
|
+
React.createElement("button", {
|
|
10970
|
+
type: "button", className: "ps-summary-picker__preset is-clear",
|
|
10971
|
+
onClick: () => onChange({ preset: "custom", providers: [] }),
|
|
10972
|
+
}, "Clear all"),
|
|
10973
|
+
view.providers.length === 0
|
|
10974
|
+
? React.createElement("div", { className: "ps-summary-picker__empty" }, "No providers to choose from.")
|
|
10975
|
+
: view.providers.map((p) => React.createElement("label", { key: p.name, className: "ps-summary-picker__row" },
|
|
10976
|
+
React.createElement("input", {
|
|
10977
|
+
type: "checkbox",
|
|
10978
|
+
checked: checked.has(p.name),
|
|
10979
|
+
onChange: () => toggle(p.name),
|
|
10980
|
+
"aria-label": `Include ${p.name}`,
|
|
10981
|
+
}),
|
|
10982
|
+
React.createElement("span", { className: "ps-summary-picker__name" }, p.name),
|
|
10983
|
+
React.createElement("span", { className: `ps-scope-badge is-${p.class}` }, p.class === "shared" ? "shared" : "user")))) : null);
|
|
10984
|
+
}
|
|
10985
|
+
|
|
10986
|
+
function summaryTokens(n) {
|
|
10987
|
+
return formatCompactNumber(Number(n) || 0);
|
|
10988
|
+
}
|
|
10989
|
+
|
|
10990
|
+
/**
|
|
10991
|
+
* The stacked bar chart: one bar per UTC day, drawn as SVG so it needs no
|
|
10992
|
+
* library and scales with the pane. A bar is the day's total, input +
|
|
10993
|
+
* output; the input segment is drawn as two — the part that was not in
|
|
10994
|
+
* the cache, and the part served from or written to it — because the cache
|
|
10995
|
+
* figures are parts of the input, not additions to it.
|
|
10996
|
+
* Linear by default; when one day is more than ten times the median of the
|
|
10997
|
+
* others, the axis goes to log10(1 + n) so the rest are not hairlines.
|
|
10998
|
+
*/
|
|
10999
|
+
/**
|
|
11000
|
+
* Log or linear? Log when one day is more than ten times the median of the
|
|
11001
|
+
* days that had anything — otherwise a spike turns every other day into a
|
|
11002
|
+
* hairline. Shared by the chart and the card header that names the scale.
|
|
11003
|
+
*/
|
|
11004
|
+
function summaryLogScale(series) {
|
|
11005
|
+
const totals = series.map((d) => d.total);
|
|
11006
|
+
const max = Math.max(0, ...totals);
|
|
11007
|
+
const sorted = totals.filter((t) => t > 0).sort((a, b) => a - b);
|
|
11008
|
+
const median = sorted.length ? sorted[Math.floor(sorted.length / 2)] : 0;
|
|
11009
|
+
return median > 0 && max > median * 10;
|
|
11010
|
+
}
|
|
11011
|
+
|
|
11012
|
+
function SummaryChart({ series }) {
|
|
11013
|
+
const W = 720; const H = 190; const padL = 58; const padB = 22; const padT = 8;
|
|
11014
|
+
const totals = series.map((d) => d.total);
|
|
11015
|
+
const max = Math.max(0, ...totals);
|
|
11016
|
+
const log = summaryLogScale(series);
|
|
11017
|
+
const scale = (v) => (log ? Math.log10(1 + v) : v);
|
|
11018
|
+
const top = scale(max) || 1;
|
|
11019
|
+
const innerH = H - padB - padT;
|
|
11020
|
+
const y = (v) => innerH - (scale(v) / top) * innerH;
|
|
11021
|
+
const n = Math.max(1, series.length);
|
|
11022
|
+
const slot = (W - padL) / n;
|
|
11023
|
+
const barW = Math.max(3, slot * 0.66);
|
|
11024
|
+
// Ticks: quarters of the range on a linear axis; powers of ten on a log
|
|
11025
|
+
// axis, so the labels are numbers a person recognises (10K, 1M) rather
|
|
11026
|
+
// than wherever a quarter of log10(max) happens to land.
|
|
11027
|
+
const ticks = log
|
|
11028
|
+
? [0, ...Array.from({ length: Math.floor(Math.log10(Math.max(10, max))) }, (_, i) => Math.pow(10, i + 1))]
|
|
11029
|
+
.filter((v) => v <= max || v === 0)
|
|
11030
|
+
.map((v) => ({ f: scale(v) / top, label: summaryTokens(v) }))
|
|
11031
|
+
: [0, 0.25, 0.5, 0.75, 1].map((f) => ({ f, label: summaryTokens(f * max) }));
|
|
11032
|
+
const labelEvery = n > 40 ? 10 : (n > 20 ? 5 : 1);
|
|
11033
|
+
return React.createElement("svg", {
|
|
11034
|
+
className: "ps-summary-chart", viewBox: `0 0 ${W} ${H}`, role: "img",
|
|
11035
|
+
"aria-label": `Tokens per day, ${series.length} days${log ? ", log scale" : ""}`,
|
|
11036
|
+
},
|
|
11037
|
+
ticks.map((t) => React.createElement("g", { key: t.label + t.f },
|
|
11038
|
+
React.createElement("line", { x1: padL, x2: W, y1: padT + innerH - t.f * innerH, y2: padT + innerH - t.f * innerH, className: "ps-summary-chart__grid" }),
|
|
11039
|
+
React.createElement("text", { x: padL - 6, y: padT + innerH - t.f * innerH + 3, className: "ps-summary-chart__tick", textAnchor: "end" }, t.label))),
|
|
11040
|
+
series.map((d, i) => {
|
|
11041
|
+
const x = padL + i * slot + (slot - barW) / 2;
|
|
11042
|
+
// Stack bottom-up: uncached input, cache read, cache write,
|
|
11043
|
+
// output. The four add up to input + output — the day's total —
|
|
11044
|
+
// because the two cache parts are carved out of the input. Under
|
|
11045
|
+
// a log axis the stack is the log of the running total, so
|
|
11046
|
+
// segment heights stay honest to the whole bar.
|
|
11047
|
+
const cr = Math.min(d.input, d.cacheRead);
|
|
11048
|
+
const cw = Math.min(d.input - cr, d.cacheWrite);
|
|
11049
|
+
const cum1 = d.input - cr - cw; const cum2 = cum1 + cr; const cum3 = d.input; const cum4 = cum3 + d.output;
|
|
11050
|
+
const yb = padT + innerH;
|
|
11051
|
+
const y1 = padT + y(cum1); const y2 = padT + y(cum2); const y3 = padT + y(cum3); const y4 = padT + y(cum4);
|
|
11052
|
+
const title = `${d.day}: ${summaryTokens(d.total)} tokens · in ${summaryTokens(d.input)} (cache r ${summaryTokens(d.cacheRead)} · w ${summaryTokens(d.cacheWrite)}) · out ${summaryTokens(d.output)} · ${d.turns} turns`;
|
|
11053
|
+
return React.createElement("g", { key: d.day, className: "ps-summary-chart__bar" },
|
|
11054
|
+
React.createElement("title", null, title),
|
|
11055
|
+
React.createElement("rect", { x, y: y1, width: barW, height: Math.max(0, yb - y1), className: "is-in" }),
|
|
11056
|
+
React.createElement("rect", { x, y: y2, width: barW, height: Math.max(0, y1 - y2), className: "is-cache" }),
|
|
11057
|
+
React.createElement("rect", { x, y: y3, width: barW, height: Math.max(0, y2 - y3), className: "is-cache-w" }),
|
|
11058
|
+
React.createElement("rect", { x, y: y4, width: barW, height: Math.max(0, y3 - y4), className: "is-out" }),
|
|
11059
|
+
(i % labelEvery === 0 || i === n - 1)
|
|
11060
|
+
? React.createElement("text", { x: x + barW / 2, y: H - 6, className: "ps-summary-chart__day", textAnchor: "middle" }, d.day.slice(5))
|
|
11061
|
+
: null);
|
|
11062
|
+
}));
|
|
11063
|
+
}
|
|
11064
|
+
|
|
11065
|
+
function SummarySparkline({ values }) {
|
|
11066
|
+
const max = Math.max(0, ...values);
|
|
11067
|
+
// 90 days at 4px is a 450px column; narrower bars past a month.
|
|
11068
|
+
return React.createElement("span", {
|
|
11069
|
+
className: "ps-summary-spark", "aria-hidden": "true",
|
|
11070
|
+
style: { gridAutoColumns: values.length > 30 ? "2px" : "4px" },
|
|
11071
|
+
},
|
|
11072
|
+
values.map((v, i) => React.createElement("i", { key: i, style: { height: `${max > 0 ? Math.max(v > 0 ? 8 : 0, (v / max) * 100) : 0}%` } })));
|
|
11073
|
+
}
|
|
11074
|
+
|
|
11075
|
+
function SummaryKpi({ label, w }) {
|
|
11076
|
+
return React.createElement("div", { className: "ps-summary-kpi" },
|
|
11077
|
+
React.createElement("div", { className: "ps-summary-kpi__label" }, label),
|
|
11078
|
+
React.createElement("div", { className: "ps-summary-kpi__value" }, summaryTokens(w.total)),
|
|
11079
|
+
// total = in + out; the cache pair is what the input was made of.
|
|
11080
|
+
React.createElement("div", { className: "ps-summary-kpi__split", title: "Total is input + output. Cache read and write are parts of the input, not additions to it." },
|
|
11081
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-in" }), "in ", summaryTokens(w.input)),
|
|
11082
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-cache" }), "cache r ", summaryTokens(w.cacheRead)),
|
|
11083
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-cache-w" }), "w ", summaryTokens(w.cacheWrite)),
|
|
11084
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-out" }), "out ", summaryTokens(w.output)),
|
|
11085
|
+
React.createElement("span", null, `· ${w.turns} turns`)));
|
|
11086
|
+
}
|
|
11087
|
+
|
|
11088
|
+
function summaryViewDeps(state) {
|
|
11089
|
+
const s = state.budget?.summary || {};
|
|
11090
|
+
return [s.loading, s.error, s.fetchedAt, s.days, s.preset, (s.providers || []).join(","), (state.budget?.grid || []).length];
|
|
11091
|
+
}
|
|
11092
|
+
|
|
11093
|
+
/**
|
|
11094
|
+
* The Cluster summary tab. Everything on it comes from ONE answer to
|
|
11095
|
+
* getProviderUsageSummary, so the KPIs, the chart and the model table can
|
|
11096
|
+
* never disagree about which turns were counted.
|
|
11097
|
+
*/
|
|
11098
|
+
function ClusterSummaryPane({ controller }) {
|
|
11099
|
+
const deps = useControllerSelector(controller, summaryViewDeps, shallowEqualObject);
|
|
11100
|
+
const view = React.useMemo(() => selectUsageSummary(controller.getState()), [controller, deps]);
|
|
11101
|
+
React.useEffect(() => {
|
|
11102
|
+
if (!view.loaded && !view.loading && !view.error) controller.loadUsageSummary().catch(() => {});
|
|
11103
|
+
}, [controller]);
|
|
11104
|
+
|
|
11105
|
+
const setRange = (days) => controller.setUsageSummaryFilter({ days }).catch(() => {});
|
|
11106
|
+
const setProviders = ({ preset, providers }) => controller.setUsageSummaryFilter({ preset, providers }).catch(() => {});
|
|
11107
|
+
const classes = view.classes.filter((c) => c.total > 0);
|
|
11108
|
+
const rangeLabel = `last ${view.days} days`;
|
|
11109
|
+
|
|
11110
|
+
return React.createElement("div", { className: `ps-summary${view.loading ? " is-loading" : ""}` },
|
|
11111
|
+
React.createElement("div", { className: "ps-summary__filters" },
|
|
11112
|
+
React.createElement(SummaryProviderPicker, { view, onChange: setProviders }),
|
|
11113
|
+
React.createElement("div", { className: "ps-budget-seg", role: "group", "aria-label": "Window" },
|
|
11114
|
+
SUMMARY_RANGES.map((days) => React.createElement("button", {
|
|
11115
|
+
key: days, type: "button",
|
|
11116
|
+
className: `ps-budget-seg__button${view.days === days ? " is-on" : ""}`,
|
|
11117
|
+
"aria-pressed": view.days === days,
|
|
11118
|
+
"aria-label": `Last ${days} days`,
|
|
11119
|
+
onClick: () => setRange(days),
|
|
11120
|
+
}, `${days}d`))),
|
|
11121
|
+
React.createElement("span", { className: "ps-summary__scope" },
|
|
11122
|
+
view.scope === "cluster" ? "Whole cluster, system sessions included" : "Your own turns")),
|
|
11123
|
+
view.error ? React.createElement("div", { className: "ps-budget-empty is-error", role: "alert" },
|
|
11124
|
+
React.createElement("div", null, "The summary could not be read. "),
|
|
11125
|
+
React.createElement("div", { className: "ps-budget-sub" }, view.error)) : null,
|
|
11126
|
+
React.createElement("div", { className: "ps-summary__kpis" },
|
|
11127
|
+
React.createElement(SummaryKpi, { label: "Today (UTC)", w: view.windows.day }),
|
|
11128
|
+
React.createElement(SummaryKpi, { label: "Last 7 days", w: view.windows.week }),
|
|
11129
|
+
React.createElement(SummaryKpi, { label: "Last 30 days", w: view.windows.month })),
|
|
11130
|
+
React.createElement("div", { className: "ps-summary__card" },
|
|
11131
|
+
React.createElement("div", { className: "ps-summary__card-head" },
|
|
11132
|
+
React.createElement("b", null, "Tokens per day"),
|
|
11133
|
+
React.createElement("span", { className: "ps-summary__legend" },
|
|
11134
|
+
React.createElement("span", { title: "Prompt tokens that were neither served from nor written to the cache" }, React.createElement("i", { className: "ps-summary-dot is-in" }), "input"),
|
|
11135
|
+
React.createElement("span", { title: "The part of the input served from the cache" }, React.createElement("i", { className: "ps-summary-dot is-cache" }), "cache read"),
|
|
11136
|
+
React.createElement("span", { title: "The part of the input written to the cache" }, React.createElement("i", { className: "ps-summary-dot is-cache-w" }), "cache write"),
|
|
11137
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-out" }), "output")),
|
|
11138
|
+
React.createElement("span", { className: "ps-summary__muted" }, `${rangeLabel} · ${summaryTokens(view.windowTotal)} tokens`),
|
|
11139
|
+
// Named here, not painted over the bars: a spike is exactly
|
|
11140
|
+
// when the tallest bar reaches the top-right corner.
|
|
11141
|
+
summaryLogScale(view.series)
|
|
11142
|
+
? React.createElement("span", { className: "ps-summary__tag", title: "One day is more than ten times the median, so the axis is log10 — otherwise every other day is a hairline." }, "log scale")
|
|
11143
|
+
: null),
|
|
11144
|
+
view.windowTotal > 0 || view.loaded
|
|
11145
|
+
? React.createElement(SummaryChart, { series: view.series })
|
|
11146
|
+
: null,
|
|
11147
|
+
classes.length > 1 ? React.createElement("div", { className: "ps-summary__muted" },
|
|
11148
|
+
"By charge: ", classes.map((c, i) => `${i ? " · " : ""}${c.chargeClass} ${summaryTokens(c.total)}`).join("")) : null),
|
|
11149
|
+
React.createElement("div", { className: "ps-summary__card" },
|
|
11150
|
+
React.createElement("div", { className: "ps-summary__card-head" },
|
|
11151
|
+
React.createElement("b", null, "By model"),
|
|
11152
|
+
React.createElement("span", { className: "ps-summary__muted" }, `${rangeLabel} · across providers, efforts and context tiers`)),
|
|
11153
|
+
view.models.length === 0
|
|
11154
|
+
? React.createElement("div", { className: "ps-budget-empty" }, view.loaded ? "No token usage in this window." : (view.loading ? "Reading…" : ""))
|
|
11155
|
+
: React.createElement("div", { className: "ps-summary__table-wrap" },
|
|
11156
|
+
React.createElement("table", { className: "ps-summary__table" },
|
|
11157
|
+
React.createElement("thead", null, React.createElement("tr", null,
|
|
11158
|
+
[["Model"], ["Turns"], ["Providers", "Distinct provider names in the ledger for this window — including providers since deleted"],
|
|
11159
|
+
["Input", "Prompt tokens, including what was served from and written to the cache"], ["Output"],
|
|
11160
|
+
["Cache read", "The part of Input served from the cache"], ["Cache write", "The part of Input written to the cache"],
|
|
11161
|
+
["Total", "Input + Output"], ["Share"], ["Trend"]]
|
|
11162
|
+
.map(([h, title]) => React.createElement("th", { key: h, title }, h)))),
|
|
11163
|
+
React.createElement("tbody", null, view.models.map((m) => React.createElement("tr", { key: m.model },
|
|
11164
|
+
React.createElement("td", { className: "is-model" }, m.model),
|
|
11165
|
+
React.createElement("td", null, m.turns),
|
|
11166
|
+
React.createElement("td", null, m.providers),
|
|
11167
|
+
React.createElement("td", null, summaryTokens(m.input)),
|
|
11168
|
+
React.createElement("td", null, summaryTokens(m.output)),
|
|
11169
|
+
React.createElement("td", null, summaryTokens(m.cacheRead)),
|
|
11170
|
+
React.createElement("td", null, summaryTokens(m.cacheWrite)),
|
|
11171
|
+
React.createElement("td", { className: "is-total" }, summaryTokens(m.total)),
|
|
11172
|
+
React.createElement("td", { className: "is-share" },
|
|
11173
|
+
React.createElement("i", { className: "ps-summary-share", style: { width: `${Math.max(1, Math.round(m.share * 100))}px` } }),
|
|
11174
|
+
`${(m.share * 100).toFixed(m.share < 0.1 ? 1 : 0)}%`),
|
|
11175
|
+
React.createElement("td", null, React.createElement(SummarySparkline, { values: m.sparkline })))))))));
|
|
11176
|
+
}
|
|
11177
|
+
|
|
10948
11178
|
/**
|
|
10949
11179
|
* The whole surface: a head, one table, and the selected provider's days.
|
|
10950
11180
|
*
|
|
@@ -11146,24 +11376,17 @@ function ProviderBudgetView({ controller }) {
|
|
|
11146
11376
|
React.createElement("span", { className: "ps-budget-coin", "aria-hidden": "true" }, "◎"),
|
|
11147
11377
|
React.createElement("h2", { className: "ps-budget-h" }, "Providers & Budgets"),
|
|
11148
11378
|
React.createElement("span", { className: "ps-budget-sub" }, "Usage and limits")),
|
|
11379
|
+
// Refresh only. Leaving (to the workspace, or to the admin console)
|
|
11380
|
+
// is the toolbar's Mode cluster; a ✕ and a gear here were second,
|
|
11381
|
+
// unlabelled copies of buttons already on screen. Esc still closes.
|
|
11149
11382
|
React.createElement("div", { className: "ps-budget-head-actions" },
|
|
11150
|
-
React.createElement(IconButton, {
|
|
11151
|
-
icon: React.createElement(CogGlyph),
|
|
11152
|
-
label: "Model providers",
|
|
11153
|
-
className: "ps-mini-button",
|
|
11154
|
-
onClick: () => { controller.closeBudget(); void controller.openAdminConsole(); },
|
|
11155
|
-
}),
|
|
11156
11383
|
React.createElement(IconButton, {
|
|
11157
11384
|
icon: "↻",
|
|
11158
11385
|
label: "Refresh providers and budgets",
|
|
11159
11386
|
className: "ps-mini-button",
|
|
11160
11387
|
disabled: view.loading || view.refreshing,
|
|
11161
11388
|
onClick: () => controller.refreshProviderTable().catch(() => {}),
|
|
11162
|
-
})
|
|
11163
|
-
React.createElement("button", {
|
|
11164
|
-
type: "button", className: "ps-budget-close", onClick: () => controller.closeBudget(),
|
|
11165
|
-
"aria-label": "Close providers and budgets", title: "Close (Esc)",
|
|
11166
|
-
}, "✕")));
|
|
11389
|
+
})));
|
|
11167
11390
|
|
|
11168
11391
|
// A read that FAILED is never dressed up as an empty one: "no providers"
|
|
11169
11392
|
// and "we could not find out" are different facts, and only one of them is
|
|
@@ -11255,7 +11478,18 @@ function ProviderBudgetView({ controller }) {
|
|
|
11255
11478
|
|
|
11256
11479
|
return React.createElement("div", { className: "ps-budget-surface" },
|
|
11257
11480
|
head,
|
|
11258
|
-
|
|
11481
|
+
// Two tabs: the per-provider table (meters, people's turns, limits)
|
|
11482
|
+
// and the Cluster summary (the ledger, every turn, by model).
|
|
11483
|
+
React.createElement("div", { className: "ps-budget-tabs", role: "tablist" },
|
|
11484
|
+
[["providers", "Providers"], ["summary", "Cluster summary"]].map(([id, label]) => React.createElement("button", {
|
|
11485
|
+
key: id, type: "button", role: "tab",
|
|
11486
|
+
"aria-selected": (controller.getState().budget?.tab || "providers") === id,
|
|
11487
|
+
className: `ps-budget-tab${(controller.getState().budget?.tab || "providers") === id ? " is-on" : ""}`,
|
|
11488
|
+
onClick: () => controller.setBudgetTab(id),
|
|
11489
|
+
}, label))),
|
|
11490
|
+
(controller.getState().budget?.tab || "providers") === "summary"
|
|
11491
|
+
? React.createElement("div", { className: "ps-budget" }, React.createElement(ClusterSummaryPane, { controller }))
|
|
11492
|
+
: React.createElement("div", { className: "ps-budget" }, body),
|
|
11259
11493
|
sheetEl);
|
|
11260
11494
|
}
|
|
11261
11495
|
|
|
@@ -12342,18 +12576,11 @@ function AdminConsolePanel({ controller, mobile = false }) {
|
|
|
12342
12576
|
closeProviderSheet();
|
|
12343
12577
|
}, [closeProviderSheet, controller, providerSheet]);
|
|
12344
12578
|
|
|
12345
|
-
|
|
12346
|
-
|
|
12579
|
+
// Title only. The signed-in person is already in the portal header, and
|
|
12580
|
+
// the way out is the toolbar's Mode cluster (Workspace) \u2014 a \u2715 here was a
|
|
12581
|
+
// second, unlabelled way to do the same thing.
|
|
12347
12582
|
const header = React.createElement("header", { className: "ps-admin-console__header" },
|
|
12348
|
-
React.createElement("h2", null, view.isAdmin ? "Admin Console" : "Settings")
|
|
12349
|
-
React.createElement("span", { className: "ps-admin-console__who" }, principalLabel),
|
|
12350
|
-
React.createElement("button", {
|
|
12351
|
-
type: "button",
|
|
12352
|
-
className: "ps-mini-button is-icon",
|
|
12353
|
-
onClick: onClose,
|
|
12354
|
-
title: view.isAdmin ? "Close the admin console" : "Close settings",
|
|
12355
|
-
"aria-label": view.isAdmin ? "Close the admin console" : "Close settings",
|
|
12356
|
-
}, "\u2715"));
|
|
12583
|
+
React.createElement("h2", null, view.isAdmin ? "Admin Console" : "Settings"));
|
|
12357
12584
|
|
|
12358
12585
|
const providerSection = React.createElement(AdminModelProvidersSection, {
|
|
12359
12586
|
controller,
|
|
@@ -13182,7 +13409,9 @@ function AdminProviderRows({ providers, isAdmin, personal, busy, onSystemUse, on
|
|
|
13182
13409
|
// drops the cluster-default flag, the allowance, any hold, the
|
|
13183
13410
|
// system-use routing and the usage history, i.e. everything this
|
|
13184
13411
|
// feature exists to preserve.
|
|
13185
|
-
|
|
13412
|
+
// A workload-identity provider has no stored key: the worker
|
|
13413
|
+
// authenticates as itself, and the server refuses the update.
|
|
13414
|
+
(personal || isAdmin) && !provider.usesWorkloadIdentity ? React.createElement("button", {
|
|
13186
13415
|
type: "button",
|
|
13187
13416
|
className: "ps-mini-button",
|
|
13188
13417
|
// Several providers means several identical "Update Key"
|
|
@@ -13279,6 +13508,12 @@ function AdminModelProvidersSection({ controller, view, onAddPersonal, onAddShar
|
|
|
13279
13508
|
React.createElement(AdminProviderRows, {
|
|
13280
13509
|
providers: providers.sharedProviders || [], isAdmin: true, personal: false, busy: pending,
|
|
13281
13510
|
onSystemUse: () => {}, onDelete: deleteProvider,
|
|
13511
|
+
// The same handler as the personal rows: it reads
|
|
13512
|
+
// provider.class and opens the sheet in shared mode. 0.5.47
|
|
13513
|
+
// showed the button on shared rows without wiring this, so
|
|
13514
|
+
// the click threw "onUpdate is not a function" and nothing
|
|
13515
|
+
// opened.
|
|
13516
|
+
onUpdate: onUpdatePersonal,
|
|
13282
13517
|
})),
|
|
13283
13518
|
React.createElement("div", { className: "ps-admin-provider-block" },
|
|
13284
13519
|
React.createElement(AdminDefaultSelect, {
|
|
@@ -13728,29 +13963,15 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
13728
13963
|
defaultProfileSettingsRef.current = buildDefaultProfileSettingsFromState(
|
|
13729
13964
|
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
13730
13965
|
);
|
|
13731
|
-
//
|
|
13732
|
-
//
|
|
13733
|
-
//
|
|
13734
|
-
//
|
|
13735
|
-
//
|
|
13736
|
-
// Folded into the settings rather than dispatched separately.
|
|
13737
|
-
// A separate dispatch raced the re-baseline at the end of this
|
|
13738
|
-
// same poll: the new value was recorded as "already saved",
|
|
13739
|
-
// never written, and the next poll put it back. Going through
|
|
13740
|
-
// the settings means the normal apply-and-save path carries it.
|
|
13741
|
-
//
|
|
13742
|
-
// Only when NOTHING is stored. An explicit choice, including
|
|
13743
|
-
// closing the canvas, is a preference from then on. Not
|
|
13744
|
-
// live-responsive either: narrowing the window never closes a
|
|
13745
|
-
// canvas the user opened on purpose.
|
|
13746
|
-
const wantsDefaultCanvas = !hasStoredDesktopPanes(remoteNormalized)
|
|
13747
|
-
&& !isNarrowViewport()
|
|
13748
|
-
&& window.innerWidth >= PORTAL_CANVAS_DEFAULT_MIN_VIEWPORT_PX;
|
|
13966
|
+
// The default workspace is sessions + chat, nothing else. A
|
|
13967
|
+
// first visit on a wide desktop used to open the canvas as
|
|
13968
|
+
// well; that was withdrawn (2026-08-27) — the canvas and
|
|
13969
|
+
// diagnostics open when a person opens them (or an agent
|
|
13970
|
+
// draws, see canvas/flip), and a stored choice is honoured.
|
|
13749
13971
|
const settings = materializeProfileSettings(
|
|
13750
13972
|
profile?.profileSettings,
|
|
13751
13973
|
defaultProfileSettingsRef.current,
|
|
13752
13974
|
);
|
|
13753
|
-
if (wantsDefaultCanvas) settings.desktopPanes = { canvasOpen: true, diagnosticsOpen: false };
|
|
13754
13975
|
const settingsJson = JSON.stringify(settings);
|
|
13755
13976
|
const currentSettingsBeforeApply = profileSettingsFromViewState(
|
|
13756
13977
|
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
@@ -14014,7 +14235,16 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14014
14235
|
// rail width is remembered for THIS session only.
|
|
14015
14236
|
? `${zenRailPx}px var(--ps-resizer-track, 16px) minmax(0, 1fr)`
|
|
14016
14237
|
: (rightSideActive
|
|
14017
|
-
|
|
14238
|
+
// The right block gets its pixels until CHAT is at its
|
|
14239
|
+
// floor — not "60% of the window". With canvas and
|
|
14240
|
+
// diagnostics both open, a 60% cap bound early (a
|
|
14241
|
+
// 640px canvas + 320px diagnostics is 60% of a
|
|
14242
|
+
// 1600px window), and past it a drag on the chat
|
|
14243
|
+
// seam could not widen the block: the canvas kept its
|
|
14244
|
+
// fixed pixels and the diagnostics column, the flex
|
|
14245
|
+
// one, gave them up — so the chat↔canvas seam moved
|
|
14246
|
+
// the canvas↔diagnostics seam instead.
|
|
14247
|
+
? `minmax(0, 1fr) var(--ps-resizer-track, 16px) min(${rightSidePx}px, calc(100% - var(--ps-resizer-track, 16px) - ${PORTAL_MIN_CHAT_COLUMN_PX}px))`
|
|
14018
14248
|
: "minmax(0, 1fr)")),
|
|
14019
14249
|
},
|
|
14020
14250
|
},
|