pilotswarm 0.5.47 → 0.5.48
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 +66 -0
- 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 +112 -0
- package/ui/core/src/state.js +20 -0
- package/ui/core/src/table-layout.js +155 -0
- package/ui/react/src/web-app.js +427 -223
- package/web/dist/assets/{abnfDiagram-VRR7QNED-VPL8m_9W.js → abnfDiagram-VRR7QNED-6rYy4Wj8.js} +1 -1
- package/web/dist/assets/{arc-T5I3-c1B.js → arc-CrRZ1Za7.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DEP70bg6.js → architectureDiagram-ZJ3FMSHR-CPe2QuQL.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-CvBu1MbO.js → blockDiagram-677ZJIJ3-DfT6vWOg.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-B5qwF4gm.js → c4Diagram-LMCZKHZV-BO-VXBT8.js} +1 -1
- package/web/dist/assets/channel-HtOTPdHg.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-zVW8dS7X.js → chunk-2Q5K7J3B-nq8p0xwC.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-DWctxZZS.js → chunk-32BRIVSS-DstJjLIA.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-Bq7_Vmf9.js → chunk-5VM5RSS4-Bq0MPH_Q.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-B8CFB3v-.js → chunk-EX3LRPZG-BPofXJWl.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-B7gLPjgZ.js → chunk-JWPE2WC7-BBr7v6mo.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-B2x6q12r.js → chunk-MOJQB5TN-DA7ca6Jr.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-XorU9Stx.js → chunk-RYQCIY6F-fdXZe4nw.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-CsLa6pw5.js → chunk-V7JOEXUC-BSATEdk2.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-BBv9mwmj.js → chunk-VR4S4FIN-RR27eQST.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-Cjm4mm6l.js → chunk-XXDRQBXY-Bh8VimxP.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C2P3dws0.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C2P3dws0.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Zw9ZQbVl.js → cose-bilkent-JH36ORCC-eP239MQJ.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-BYnR9YLM.js → cynefin-VYW2F7L2-Sb8l4Vm6.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Cx8L0dOv.js → cynefinDiagram-TSTJHNR4-BrWMkhw9.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-7GaqVcqz.js → dagre-VKFMJZFB-DT5kNKEx.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BWSRPJvU.js → diagram-FQU43EPY-Dy0Bxyod.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-BELuAE1e.js → diagram-G47NLZAW-R_yWa2-6.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-DGXhPpri.js → diagram-NH7WQ7WH-BV2cGmgX.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DV8HqN0T.js → diagram-OA4YK3LP-auayiDmq.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Clv1X3ZL.js → diagram-WEI45ONY-CE7bHIYE.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-TpQx36oi.js → ebnfDiagram-CCIWWBDH-C3SMk6Uu.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DVVWd00c.js → erDiagram-Q63AITRT-CnTs6X2D.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-Cdz_8_Dw.js → flowDiagram-23GEKE2U-DobYm7mq.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-zlco-o7u.js → ganttDiagram-NO4QXBWP-AkTQtAsR.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CfAZW1En.js → gitGraphDiagram-IHSO6WYX-CpLWsqgx.js} +1 -1
- package/web/dist/assets/{index-tQXCQojP.js → index-BCzNnCl0.js} +6 -6
- package/web/dist/assets/index-BUp5hFdX.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-0R-8efwI.js → infoDiagram-FWYZ7A6U-ByzTSgj_.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-lPpoXGlp.js → ishikawaDiagram-FXEZZL3T-CaL9Wi8i.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-D05vSVqu.js → journeyDiagram-5HDEW3XC-BillueZJ.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-ClaDdESv.js → kanban-definition-HUTT4EX6-DI_IRVWQ.js} +1 -1
- package/web/dist/assets/{linear-CUnhHMvY.js → linear-Cabaq-RD.js} +1 -1
- package/web/dist/assets/{mermaid.core-DaYS-X8Y.js → mermaid.core-D8KnBjxP.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-N480VHds.js → mindmap-definition-LN4V7U3C-Bl7FpPwk.js} +1 -1
- package/web/dist/assets/{papaparse.min-CNTcNgd8.js → papaparse.min-CcjjKk0h.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-CVWyJ5ck.js → pegDiagram-2B236MQR-CmsG2owD.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-UR9uohPf.js → pieDiagram-ENE6RG2P-CLA_mcHl.js} +1 -1
- package/web/dist/assets/pilotswarm-DMCBXi5A.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-D2SIOetn.js → quadrantDiagram-ABIIQ3AL-hT9bnnUG.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-OgN6qgOU.js → railroadDiagram-RFXS5EU6-CNC0kFW6.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BSUURBBz.js → requirementDiagram-TGXJPOKE-C-nSthhK.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BBZhmSBZ.js → sankeyDiagram-HTMAVEWB-C5JdV5bI.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-C10IDNdW.js → sequenceDiagram-DBY2YBRQ-BMKBxKk9.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-B9787MHa.js → sizeCapture-X5ZJPWSS-CbwNw-TR.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BmSBo7es.js → stateDiagram-2N3HPSRC-ChvGrUa_.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DLbEwVA2.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-DOXpv_KQ.js → swimlanes-5IMT3BWC-D9Uzkbi7.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DMzhdO6y.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-SjhGfPdI.js → timeline-definition-FHXFAJF6-ctFEfgUp.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BiujYen6.js → vennDiagram-L72KCM5P-DGrJhlyF.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-6nLXBvwc.js → wardleyDiagram-EHGQE667-BGqtZ8dQ.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Di7ioTvW.js → xychartDiagram-FW5EYKEG-DAXXs9xC.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
|
/**
|
|
@@ -10941,10 +10868,289 @@ function budgetViewDeps(state) {
|
|
|
10941
10868
|
Boolean(state.ui?.budgetOpen), budget.loading, budget.refreshing, budget.loaded,
|
|
10942
10869
|
budget.error, budget.grid, budget.paused, budget.overall, budget.selectedProvider,
|
|
10943
10870
|
budget.selectedScope, budget.missingProvider, budget.series,
|
|
10871
|
+
budget.tab, budget.summary,
|
|
10944
10872
|
state.auth?.authorization?.role, state.admin?.profile,
|
|
10945
10873
|
];
|
|
10946
10874
|
}
|
|
10947
10875
|
|
|
10876
|
+
// ─── Cluster summary tab ────────────────────────────────────────────────
|
|
10877
|
+
|
|
10878
|
+
const SUMMARY_RANGES = [14, 30, 90];
|
|
10879
|
+
const SUMMARY_PRESETS = [
|
|
10880
|
+
{ id: "all", label: "All providers" },
|
|
10881
|
+
{ id: "shared", label: "Shared providers" },
|
|
10882
|
+
{ id: "users", label: "User providers" },
|
|
10883
|
+
];
|
|
10884
|
+
|
|
10885
|
+
function summaryPresetLabel(view) {
|
|
10886
|
+
if (view.preset === "all") return "All providers";
|
|
10887
|
+
if (view.preset === "shared") return "Shared providers";
|
|
10888
|
+
if (view.preset === "users") return "User providers";
|
|
10889
|
+
const n = view.selectedProviders.length;
|
|
10890
|
+
return n === 0 ? "No providers" : (n === 1 ? view.selectedProviders[0] : `${n} providers`);
|
|
10891
|
+
}
|
|
10892
|
+
|
|
10893
|
+
/**
|
|
10894
|
+
* The provider picker: three presets and a checkbox per provider. A preset
|
|
10895
|
+
* expands to an exact list of names before the request goes out — the
|
|
10896
|
+
* database never has to know what "Shared" meant on the day.
|
|
10897
|
+
*/
|
|
10898
|
+
function SummaryProviderPicker({ view, onChange }) {
|
|
10899
|
+
const [open, setOpen] = React.useState(false);
|
|
10900
|
+
const ref = React.useRef(null);
|
|
10901
|
+
React.useEffect(() => {
|
|
10902
|
+
if (!open) return undefined;
|
|
10903
|
+
const onDown = (event) => { if (ref.current && !ref.current.contains(event.target)) setOpen(false); };
|
|
10904
|
+
const onKey = (event) => { if (event.key === "Escape") setOpen(false); };
|
|
10905
|
+
document.addEventListener("pointerdown", onDown);
|
|
10906
|
+
document.addEventListener("keydown", onKey);
|
|
10907
|
+
return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
|
|
10908
|
+
}, [open]);
|
|
10909
|
+
|
|
10910
|
+
const namesFor = (preset) => (preset === "all" ? [] : view.providers
|
|
10911
|
+
.filter((p) => (preset === "shared" ? p.class === "shared" : p.class !== "shared"))
|
|
10912
|
+
.map((p) => p.name));
|
|
10913
|
+
const checked = new Set(view.preset === "all"
|
|
10914
|
+
? view.providers.map((p) => p.name)
|
|
10915
|
+
: (view.preset === "custom" ? view.selectedProviders : namesFor(view.preset)));
|
|
10916
|
+
|
|
10917
|
+
// A preset is a whole answer, so the menu closes on it; a checkbox is
|
|
10918
|
+
// one step of a hand-picked list, so the menu stays open for the next.
|
|
10919
|
+
const choosePreset = (preset) => { onChange({ preset, providers: namesFor(preset) }); setOpen(false); };
|
|
10920
|
+
const toggle = (name) => {
|
|
10921
|
+
const next = new Set(checked);
|
|
10922
|
+
if (next.has(name)) next.delete(name); else next.add(name);
|
|
10923
|
+
onChange({ preset: "custom", providers: Array.from(next) });
|
|
10924
|
+
};
|
|
10925
|
+
|
|
10926
|
+
return React.createElement("div", { className: "ps-summary-picker", ref },
|
|
10927
|
+
React.createElement("button", {
|
|
10928
|
+
type: "button",
|
|
10929
|
+
className: `ps-mini-button ps-summary-picker__button${open ? " is-open" : ""}`,
|
|
10930
|
+
"aria-haspopup": "listbox",
|
|
10931
|
+
"aria-expanded": open,
|
|
10932
|
+
"aria-label": "Providers filter",
|
|
10933
|
+
onClick: () => setOpen((v) => !v),
|
|
10934
|
+
}, summaryPresetLabel(view), " ", React.createElement("span", { "aria-hidden": "true" }, "▾")),
|
|
10935
|
+
open ? React.createElement("div", { className: "ps-summary-picker__menu", role: "listbox", "aria-label": "Providers" },
|
|
10936
|
+
SUMMARY_PRESETS.map((preset) => React.createElement("button", {
|
|
10937
|
+
key: preset.id,
|
|
10938
|
+
type: "button",
|
|
10939
|
+
role: "option",
|
|
10940
|
+
"aria-selected": view.preset === preset.id,
|
|
10941
|
+
className: `ps-summary-picker__preset${view.preset === preset.id ? " is-on" : ""}`,
|
|
10942
|
+
onClick: () => choosePreset(preset.id),
|
|
10943
|
+
}, preset.label)),
|
|
10944
|
+
React.createElement("div", { className: "ps-summary-picker__rule", "aria-hidden": "true" }),
|
|
10945
|
+
React.createElement("button", {
|
|
10946
|
+
type: "button", className: "ps-summary-picker__preset is-clear",
|
|
10947
|
+
onClick: () => onChange({ preset: "custom", providers: [] }),
|
|
10948
|
+
}, "Clear all"),
|
|
10949
|
+
view.providers.length === 0
|
|
10950
|
+
? React.createElement("div", { className: "ps-summary-picker__empty" }, "No providers to choose from.")
|
|
10951
|
+
: view.providers.map((p) => React.createElement("label", { key: p.name, className: "ps-summary-picker__row" },
|
|
10952
|
+
React.createElement("input", {
|
|
10953
|
+
type: "checkbox",
|
|
10954
|
+
checked: checked.has(p.name),
|
|
10955
|
+
onChange: () => toggle(p.name),
|
|
10956
|
+
"aria-label": `Include ${p.name}`,
|
|
10957
|
+
}),
|
|
10958
|
+
React.createElement("span", { className: "ps-summary-picker__name" }, p.name),
|
|
10959
|
+
React.createElement("span", { className: `ps-scope-badge is-${p.class}` }, p.class === "shared" ? "shared" : "user")))) : null);
|
|
10960
|
+
}
|
|
10961
|
+
|
|
10962
|
+
function summaryTokens(n) {
|
|
10963
|
+
return formatCompactNumber(Number(n) || 0);
|
|
10964
|
+
}
|
|
10965
|
+
|
|
10966
|
+
/**
|
|
10967
|
+
* The stacked bar chart: one bar per UTC day, drawn as SVG so it needs no
|
|
10968
|
+
* library and scales with the pane. A bar is the day's total, input +
|
|
10969
|
+
* output; the input segment is drawn as two — the part that was not in
|
|
10970
|
+
* the cache, and the part served from or written to it — because the cache
|
|
10971
|
+
* figures are parts of the input, not additions to it.
|
|
10972
|
+
* Linear by default; when one day is more than ten times the median of the
|
|
10973
|
+
* others, the axis goes to log10(1 + n) so the rest are not hairlines.
|
|
10974
|
+
*/
|
|
10975
|
+
/**
|
|
10976
|
+
* Log or linear? Log when one day is more than ten times the median of the
|
|
10977
|
+
* days that had anything — otherwise a spike turns every other day into a
|
|
10978
|
+
* hairline. Shared by the chart and the card header that names the scale.
|
|
10979
|
+
*/
|
|
10980
|
+
function summaryLogScale(series) {
|
|
10981
|
+
const totals = series.map((d) => d.total);
|
|
10982
|
+
const max = Math.max(0, ...totals);
|
|
10983
|
+
const sorted = totals.filter((t) => t > 0).sort((a, b) => a - b);
|
|
10984
|
+
const median = sorted.length ? sorted[Math.floor(sorted.length / 2)] : 0;
|
|
10985
|
+
return median > 0 && max > median * 10;
|
|
10986
|
+
}
|
|
10987
|
+
|
|
10988
|
+
function SummaryChart({ series }) {
|
|
10989
|
+
const W = 720; const H = 190; const padL = 58; const padB = 22; const padT = 8;
|
|
10990
|
+
const totals = series.map((d) => d.total);
|
|
10991
|
+
const max = Math.max(0, ...totals);
|
|
10992
|
+
const log = summaryLogScale(series);
|
|
10993
|
+
const scale = (v) => (log ? Math.log10(1 + v) : v);
|
|
10994
|
+
const top = scale(max) || 1;
|
|
10995
|
+
const innerH = H - padB - padT;
|
|
10996
|
+
const y = (v) => innerH - (scale(v) / top) * innerH;
|
|
10997
|
+
const n = Math.max(1, series.length);
|
|
10998
|
+
const slot = (W - padL) / n;
|
|
10999
|
+
const barW = Math.max(3, slot * 0.66);
|
|
11000
|
+
// Ticks: quarters of the range on a linear axis; powers of ten on a log
|
|
11001
|
+
// axis, so the labels are numbers a person recognises (10K, 1M) rather
|
|
11002
|
+
// than wherever a quarter of log10(max) happens to land.
|
|
11003
|
+
const ticks = log
|
|
11004
|
+
? [0, ...Array.from({ length: Math.floor(Math.log10(Math.max(10, max))) }, (_, i) => Math.pow(10, i + 1))]
|
|
11005
|
+
.filter((v) => v <= max || v === 0)
|
|
11006
|
+
.map((v) => ({ f: scale(v) / top, label: summaryTokens(v) }))
|
|
11007
|
+
: [0, 0.25, 0.5, 0.75, 1].map((f) => ({ f, label: summaryTokens(f * max) }));
|
|
11008
|
+
const labelEvery = n > 40 ? 10 : (n > 20 ? 5 : 1);
|
|
11009
|
+
return React.createElement("svg", {
|
|
11010
|
+
className: "ps-summary-chart", viewBox: `0 0 ${W} ${H}`, role: "img",
|
|
11011
|
+
"aria-label": `Tokens per day, ${series.length} days${log ? ", log scale" : ""}`,
|
|
11012
|
+
},
|
|
11013
|
+
ticks.map((t) => React.createElement("g", { key: t.label + t.f },
|
|
11014
|
+
React.createElement("line", { x1: padL, x2: W, y1: padT + innerH - t.f * innerH, y2: padT + innerH - t.f * innerH, className: "ps-summary-chart__grid" }),
|
|
11015
|
+
React.createElement("text", { x: padL - 6, y: padT + innerH - t.f * innerH + 3, className: "ps-summary-chart__tick", textAnchor: "end" }, t.label))),
|
|
11016
|
+
series.map((d, i) => {
|
|
11017
|
+
const x = padL + i * slot + (slot - barW) / 2;
|
|
11018
|
+
// Stack bottom-up: uncached input, cache read, cache write,
|
|
11019
|
+
// output. The four add up to input + output — the day's total —
|
|
11020
|
+
// because the two cache parts are carved out of the input. Under
|
|
11021
|
+
// a log axis the stack is the log of the running total, so
|
|
11022
|
+
// segment heights stay honest to the whole bar.
|
|
11023
|
+
const cr = Math.min(d.input, d.cacheRead);
|
|
11024
|
+
const cw = Math.min(d.input - cr, d.cacheWrite);
|
|
11025
|
+
const cum1 = d.input - cr - cw; const cum2 = cum1 + cr; const cum3 = d.input; const cum4 = cum3 + d.output;
|
|
11026
|
+
const yb = padT + innerH;
|
|
11027
|
+
const y1 = padT + y(cum1); const y2 = padT + y(cum2); const y3 = padT + y(cum3); const y4 = padT + y(cum4);
|
|
11028
|
+
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`;
|
|
11029
|
+
return React.createElement("g", { key: d.day, className: "ps-summary-chart__bar" },
|
|
11030
|
+
React.createElement("title", null, title),
|
|
11031
|
+
React.createElement("rect", { x, y: y1, width: barW, height: Math.max(0, yb - y1), className: "is-in" }),
|
|
11032
|
+
React.createElement("rect", { x, y: y2, width: barW, height: Math.max(0, y1 - y2), className: "is-cache" }),
|
|
11033
|
+
React.createElement("rect", { x, y: y3, width: barW, height: Math.max(0, y2 - y3), className: "is-cache-w" }),
|
|
11034
|
+
React.createElement("rect", { x, y: y4, width: barW, height: Math.max(0, y3 - y4), className: "is-out" }),
|
|
11035
|
+
(i % labelEvery === 0 || i === n - 1)
|
|
11036
|
+
? React.createElement("text", { x: x + barW / 2, y: H - 6, className: "ps-summary-chart__day", textAnchor: "middle" }, d.day.slice(5))
|
|
11037
|
+
: null);
|
|
11038
|
+
}));
|
|
11039
|
+
}
|
|
11040
|
+
|
|
11041
|
+
function SummarySparkline({ values }) {
|
|
11042
|
+
const max = Math.max(0, ...values);
|
|
11043
|
+
// 90 days at 4px is a 450px column; narrower bars past a month.
|
|
11044
|
+
return React.createElement("span", {
|
|
11045
|
+
className: "ps-summary-spark", "aria-hidden": "true",
|
|
11046
|
+
style: { gridAutoColumns: values.length > 30 ? "2px" : "4px" },
|
|
11047
|
+
},
|
|
11048
|
+
values.map((v, i) => React.createElement("i", { key: i, style: { height: `${max > 0 ? Math.max(v > 0 ? 8 : 0, (v / max) * 100) : 0}%` } })));
|
|
11049
|
+
}
|
|
11050
|
+
|
|
11051
|
+
function SummaryKpi({ label, w }) {
|
|
11052
|
+
return React.createElement("div", { className: "ps-summary-kpi" },
|
|
11053
|
+
React.createElement("div", { className: "ps-summary-kpi__label" }, label),
|
|
11054
|
+
React.createElement("div", { className: "ps-summary-kpi__value" }, summaryTokens(w.total)),
|
|
11055
|
+
// total = in + out; the cache pair is what the input was made of.
|
|
11056
|
+
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." },
|
|
11057
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-in" }), "in ", summaryTokens(w.input)),
|
|
11058
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-cache" }), "cache r ", summaryTokens(w.cacheRead)),
|
|
11059
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-cache-w" }), "w ", summaryTokens(w.cacheWrite)),
|
|
11060
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-out" }), "out ", summaryTokens(w.output)),
|
|
11061
|
+
React.createElement("span", null, `· ${w.turns} turns`)));
|
|
11062
|
+
}
|
|
11063
|
+
|
|
11064
|
+
function summaryViewDeps(state) {
|
|
11065
|
+
const s = state.budget?.summary || {};
|
|
11066
|
+
return [s.loading, s.error, s.fetchedAt, s.days, s.preset, (s.providers || []).join(","), (state.budget?.grid || []).length];
|
|
11067
|
+
}
|
|
11068
|
+
|
|
11069
|
+
/**
|
|
11070
|
+
* The Cluster summary tab. Everything on it comes from ONE answer to
|
|
11071
|
+
* getProviderUsageSummary, so the KPIs, the chart and the model table can
|
|
11072
|
+
* never disagree about which turns were counted.
|
|
11073
|
+
*/
|
|
11074
|
+
function ClusterSummaryPane({ controller }) {
|
|
11075
|
+
const deps = useControllerSelector(controller, summaryViewDeps, shallowEqualObject);
|
|
11076
|
+
const view = React.useMemo(() => selectUsageSummary(controller.getState()), [controller, deps]);
|
|
11077
|
+
React.useEffect(() => {
|
|
11078
|
+
if (!view.loaded && !view.loading && !view.error) controller.loadUsageSummary().catch(() => {});
|
|
11079
|
+
}, [controller]);
|
|
11080
|
+
|
|
11081
|
+
const setRange = (days) => controller.setUsageSummaryFilter({ days }).catch(() => {});
|
|
11082
|
+
const setProviders = ({ preset, providers }) => controller.setUsageSummaryFilter({ preset, providers }).catch(() => {});
|
|
11083
|
+
const classes = view.classes.filter((c) => c.total > 0);
|
|
11084
|
+
const rangeLabel = `last ${view.days} days`;
|
|
11085
|
+
|
|
11086
|
+
return React.createElement("div", { className: `ps-summary${view.loading ? " is-loading" : ""}` },
|
|
11087
|
+
React.createElement("div", { className: "ps-summary__filters" },
|
|
11088
|
+
React.createElement(SummaryProviderPicker, { view, onChange: setProviders }),
|
|
11089
|
+
React.createElement("div", { className: "ps-budget-seg", role: "group", "aria-label": "Window" },
|
|
11090
|
+
SUMMARY_RANGES.map((days) => React.createElement("button", {
|
|
11091
|
+
key: days, type: "button",
|
|
11092
|
+
className: `ps-budget-seg__button${view.days === days ? " is-on" : ""}`,
|
|
11093
|
+
"aria-pressed": view.days === days,
|
|
11094
|
+
"aria-label": `Last ${days} days`,
|
|
11095
|
+
onClick: () => setRange(days),
|
|
11096
|
+
}, `${days}d`))),
|
|
11097
|
+
React.createElement("span", { className: "ps-summary__scope" },
|
|
11098
|
+
view.scope === "cluster" ? "Whole cluster, system sessions included" : "Your own turns")),
|
|
11099
|
+
view.error ? React.createElement("div", { className: "ps-budget-empty is-error", role: "alert" },
|
|
11100
|
+
React.createElement("div", null, "The summary could not be read. "),
|
|
11101
|
+
React.createElement("div", { className: "ps-budget-sub" }, view.error)) : null,
|
|
11102
|
+
React.createElement("div", { className: "ps-summary__kpis" },
|
|
11103
|
+
React.createElement(SummaryKpi, { label: "Today (UTC)", w: view.windows.day }),
|
|
11104
|
+
React.createElement(SummaryKpi, { label: "Last 7 days", w: view.windows.week }),
|
|
11105
|
+
React.createElement(SummaryKpi, { label: "Last 30 days", w: view.windows.month })),
|
|
11106
|
+
React.createElement("div", { className: "ps-summary__card" },
|
|
11107
|
+
React.createElement("div", { className: "ps-summary__card-head" },
|
|
11108
|
+
React.createElement("b", null, "Tokens per day"),
|
|
11109
|
+
React.createElement("span", { className: "ps-summary__legend" },
|
|
11110
|
+
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"),
|
|
11111
|
+
React.createElement("span", { title: "The part of the input served from the cache" }, React.createElement("i", { className: "ps-summary-dot is-cache" }), "cache read"),
|
|
11112
|
+
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"),
|
|
11113
|
+
React.createElement("span", null, React.createElement("i", { className: "ps-summary-dot is-out" }), "output")),
|
|
11114
|
+
React.createElement("span", { className: "ps-summary__muted" }, `${rangeLabel} · ${summaryTokens(view.windowTotal)} tokens`),
|
|
11115
|
+
// Named here, not painted over the bars: a spike is exactly
|
|
11116
|
+
// when the tallest bar reaches the top-right corner.
|
|
11117
|
+
summaryLogScale(view.series)
|
|
11118
|
+
? 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")
|
|
11119
|
+
: null),
|
|
11120
|
+
view.windowTotal > 0 || view.loaded
|
|
11121
|
+
? React.createElement(SummaryChart, { series: view.series })
|
|
11122
|
+
: null,
|
|
11123
|
+
classes.length > 1 ? React.createElement("div", { className: "ps-summary__muted" },
|
|
11124
|
+
"By charge: ", classes.map((c, i) => `${i ? " · " : ""}${c.chargeClass} ${summaryTokens(c.total)}`).join("")) : null),
|
|
11125
|
+
React.createElement("div", { className: "ps-summary__card" },
|
|
11126
|
+
React.createElement("div", { className: "ps-summary__card-head" },
|
|
11127
|
+
React.createElement("b", null, "By model"),
|
|
11128
|
+
React.createElement("span", { className: "ps-summary__muted" }, `${rangeLabel} · across providers, efforts and context tiers`)),
|
|
11129
|
+
view.models.length === 0
|
|
11130
|
+
? React.createElement("div", { className: "ps-budget-empty" }, view.loaded ? "No token usage in this window." : (view.loading ? "Reading…" : ""))
|
|
11131
|
+
: React.createElement("div", { className: "ps-summary__table-wrap" },
|
|
11132
|
+
React.createElement("table", { className: "ps-summary__table" },
|
|
11133
|
+
React.createElement("thead", null, React.createElement("tr", null,
|
|
11134
|
+
[["Model"], ["Turns"], ["Providers", "Distinct provider names in the ledger for this window — including providers since deleted"],
|
|
11135
|
+
["Input", "Prompt tokens, including what was served from and written to the cache"], ["Output"],
|
|
11136
|
+
["Cache read", "The part of Input served from the cache"], ["Cache write", "The part of Input written to the cache"],
|
|
11137
|
+
["Total", "Input + Output"], ["Share"], ["Trend"]]
|
|
11138
|
+
.map(([h, title]) => React.createElement("th", { key: h, title }, h)))),
|
|
11139
|
+
React.createElement("tbody", null, view.models.map((m) => React.createElement("tr", { key: m.model },
|
|
11140
|
+
React.createElement("td", { className: "is-model" }, m.model),
|
|
11141
|
+
React.createElement("td", null, m.turns),
|
|
11142
|
+
React.createElement("td", null, m.providers),
|
|
11143
|
+
React.createElement("td", null, summaryTokens(m.input)),
|
|
11144
|
+
React.createElement("td", null, summaryTokens(m.output)),
|
|
11145
|
+
React.createElement("td", null, summaryTokens(m.cacheRead)),
|
|
11146
|
+
React.createElement("td", null, summaryTokens(m.cacheWrite)),
|
|
11147
|
+
React.createElement("td", { className: "is-total" }, summaryTokens(m.total)),
|
|
11148
|
+
React.createElement("td", { className: "is-share" },
|
|
11149
|
+
React.createElement("i", { className: "ps-summary-share", style: { width: `${Math.max(1, Math.round(m.share * 100))}px` } }),
|
|
11150
|
+
`${(m.share * 100).toFixed(m.share < 0.1 ? 1 : 0)}%`),
|
|
11151
|
+
React.createElement("td", null, React.createElement(SummarySparkline, { values: m.sparkline })))))))));
|
|
11152
|
+
}
|
|
11153
|
+
|
|
10948
11154
|
/**
|
|
10949
11155
|
* The whole surface: a head, one table, and the selected provider's days.
|
|
10950
11156
|
*
|
|
@@ -11146,24 +11352,17 @@ function ProviderBudgetView({ controller }) {
|
|
|
11146
11352
|
React.createElement("span", { className: "ps-budget-coin", "aria-hidden": "true" }, "◎"),
|
|
11147
11353
|
React.createElement("h2", { className: "ps-budget-h" }, "Providers & Budgets"),
|
|
11148
11354
|
React.createElement("span", { className: "ps-budget-sub" }, "Usage and limits")),
|
|
11355
|
+
// Refresh only. Leaving (to the workspace, or to the admin console)
|
|
11356
|
+
// is the toolbar's Mode cluster; a ✕ and a gear here were second,
|
|
11357
|
+
// unlabelled copies of buttons already on screen. Esc still closes.
|
|
11149
11358
|
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
11359
|
React.createElement(IconButton, {
|
|
11157
11360
|
icon: "↻",
|
|
11158
11361
|
label: "Refresh providers and budgets",
|
|
11159
11362
|
className: "ps-mini-button",
|
|
11160
11363
|
disabled: view.loading || view.refreshing,
|
|
11161
11364
|
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
|
-
}, "✕")));
|
|
11365
|
+
})));
|
|
11167
11366
|
|
|
11168
11367
|
// A read that FAILED is never dressed up as an empty one: "no providers"
|
|
11169
11368
|
// and "we could not find out" are different facts, and only one of them is
|
|
@@ -11255,7 +11454,18 @@ function ProviderBudgetView({ controller }) {
|
|
|
11255
11454
|
|
|
11256
11455
|
return React.createElement("div", { className: "ps-budget-surface" },
|
|
11257
11456
|
head,
|
|
11258
|
-
|
|
11457
|
+
// Two tabs: the per-provider table (meters, people's turns, limits)
|
|
11458
|
+
// and the Cluster summary (the ledger, every turn, by model).
|
|
11459
|
+
React.createElement("div", { className: "ps-budget-tabs", role: "tablist" },
|
|
11460
|
+
[["providers", "Providers"], ["summary", "Cluster summary"]].map(([id, label]) => React.createElement("button", {
|
|
11461
|
+
key: id, type: "button", role: "tab",
|
|
11462
|
+
"aria-selected": (controller.getState().budget?.tab || "providers") === id,
|
|
11463
|
+
className: `ps-budget-tab${(controller.getState().budget?.tab || "providers") === id ? " is-on" : ""}`,
|
|
11464
|
+
onClick: () => controller.setBudgetTab(id),
|
|
11465
|
+
}, label))),
|
|
11466
|
+
(controller.getState().budget?.tab || "providers") === "summary"
|
|
11467
|
+
? React.createElement("div", { className: "ps-budget" }, React.createElement(ClusterSummaryPane, { controller }))
|
|
11468
|
+
: React.createElement("div", { className: "ps-budget" }, body),
|
|
11259
11469
|
sheetEl);
|
|
11260
11470
|
}
|
|
11261
11471
|
|
|
@@ -12342,18 +12552,11 @@ function AdminConsolePanel({ controller, mobile = false }) {
|
|
|
12342
12552
|
closeProviderSheet();
|
|
12343
12553
|
}, [closeProviderSheet, controller, providerSheet]);
|
|
12344
12554
|
|
|
12345
|
-
|
|
12346
|
-
|
|
12555
|
+
// Title only. The signed-in person is already in the portal header, and
|
|
12556
|
+
// the way out is the toolbar's Mode cluster (Workspace) \u2014 a \u2715 here was a
|
|
12557
|
+
// second, unlabelled way to do the same thing.
|
|
12347
12558
|
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"));
|
|
12559
|
+
React.createElement("h2", null, view.isAdmin ? "Admin Console" : "Settings"));
|
|
12357
12560
|
|
|
12358
12561
|
const providerSection = React.createElement(AdminModelProvidersSection, {
|
|
12359
12562
|
controller,
|
|
@@ -13279,6 +13482,12 @@ function AdminModelProvidersSection({ controller, view, onAddPersonal, onAddShar
|
|
|
13279
13482
|
React.createElement(AdminProviderRows, {
|
|
13280
13483
|
providers: providers.sharedProviders || [], isAdmin: true, personal: false, busy: pending,
|
|
13281
13484
|
onSystemUse: () => {}, onDelete: deleteProvider,
|
|
13485
|
+
// The same handler as the personal rows: it reads
|
|
13486
|
+
// provider.class and opens the sheet in shared mode. 0.5.47
|
|
13487
|
+
// showed the button on shared rows without wiring this, so
|
|
13488
|
+
// the click threw "onUpdate is not a function" and nothing
|
|
13489
|
+
// opened.
|
|
13490
|
+
onUpdate: onUpdatePersonal,
|
|
13282
13491
|
})),
|
|
13283
13492
|
React.createElement("div", { className: "ps-admin-provider-block" },
|
|
13284
13493
|
React.createElement(AdminDefaultSelect, {
|
|
@@ -13728,29 +13937,15 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
13728
13937
|
defaultProfileSettingsRef.current = buildDefaultProfileSettingsFromState(
|
|
13729
13938
|
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
13730
13939
|
);
|
|
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;
|
|
13940
|
+
// The default workspace is sessions + chat, nothing else. A
|
|
13941
|
+
// first visit on a wide desktop used to open the canvas as
|
|
13942
|
+
// well; that was withdrawn (2026-08-27) — the canvas and
|
|
13943
|
+
// diagnostics open when a person opens them (or an agent
|
|
13944
|
+
// draws, see canvas/flip), and a stored choice is honoured.
|
|
13749
13945
|
const settings = materializeProfileSettings(
|
|
13750
13946
|
profile?.profileSettings,
|
|
13751
13947
|
defaultProfileSettingsRef.current,
|
|
13752
13948
|
);
|
|
13753
|
-
if (wantsDefaultCanvas) settings.desktopPanes = { canvasOpen: true, diagnosticsOpen: false };
|
|
13754
13949
|
const settingsJson = JSON.stringify(settings);
|
|
13755
13950
|
const currentSettingsBeforeApply = profileSettingsFromViewState(
|
|
13756
13951
|
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
@@ -14014,7 +14209,16 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14014
14209
|
// rail width is remembered for THIS session only.
|
|
14015
14210
|
? `${zenRailPx}px var(--ps-resizer-track, 16px) minmax(0, 1fr)`
|
|
14016
14211
|
: (rightSideActive
|
|
14017
|
-
|
|
14212
|
+
// The right block gets its pixels until CHAT is at its
|
|
14213
|
+
// floor — not "60% of the window". With canvas and
|
|
14214
|
+
// diagnostics both open, a 60% cap bound early (a
|
|
14215
|
+
// 640px canvas + 320px diagnostics is 60% of a
|
|
14216
|
+
// 1600px window), and past it a drag on the chat
|
|
14217
|
+
// seam could not widen the block: the canvas kept its
|
|
14218
|
+
// fixed pixels and the diagnostics column, the flex
|
|
14219
|
+
// one, gave them up — so the chat↔canvas seam moved
|
|
14220
|
+
// the canvas↔diagnostics seam instead.
|
|
14221
|
+
? `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
14222
|
: "minmax(0, 1fr)")),
|
|
14019
14223
|
},
|
|
14020
14224
|
},
|