pilotswarm 0.5.46 → 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/README.md +1 -0
- package/mcp/dist/src/tools/providers.d.ts.map +1 -1
- package/mcp/dist/src/tools/providers.js +27 -3
- package/mcp/dist/src/tools/providers.js.map +1 -1
- package/package.json +2 -2
- package/tui/src/app.js +7 -11
- package/tui/src/node-sdk-transport.js +21 -0
- package/ui/core/src/commands.js +1 -0
- package/ui/core/src/controller.js +331 -68
- package/ui/core/src/history.js +5 -0
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/reducer.js +93 -4
- package/ui/core/src/selectors.js +175 -15
- package/ui/core/src/state.js +31 -0
- package/ui/core/src/table-layout.js +155 -0
- package/ui/react/src/components.js +1 -0
- package/ui/react/src/web-app.js +1035 -323
- package/web/dist/assets/{abnfDiagram-VRR7QNED-GXM5pEfT.js → abnfDiagram-VRR7QNED-6rYy4Wj8.js} +1 -1
- package/web/dist/assets/{arc-D52-xt6J.js → arc-CrRZ1Za7.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bxh49GyJ.js → architectureDiagram-ZJ3FMSHR-CPe2QuQL.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-D8LSE94K.js → blockDiagram-677ZJIJ3-DfT6vWOg.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Dcs1NVIx.js → c4Diagram-LMCZKHZV-BO-VXBT8.js} +1 -1
- package/web/dist/assets/channel-HtOTPdHg.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-Dy3zu7gI.js → chunk-2Q5K7J3B-nq8p0xwC.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-5v78VX3T.js → chunk-32BRIVSS-DstJjLIA.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-DUJMCfdj.js → chunk-5VM5RSS4-Bq0MPH_Q.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DYyT1So9.js → chunk-EX3LRPZG-BPofXJWl.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Buzzx5R2.js → chunk-JWPE2WC7-BBr7v6mo.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-B8fOWRD7.js → chunk-MOJQB5TN-DA7ca6Jr.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-Bp04YO26.js → chunk-RYQCIY6F-fdXZe4nw.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-Gw6h8gPE.js → chunk-V7JOEXUC-BSATEdk2.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-ASf4dPE-.js → chunk-VR4S4FIN-RR27eQST.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-C4B-MNNk.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-C0S415ey.js → cose-bilkent-JH36ORCC-eP239MQJ.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-LWUlL1XB.js → cynefin-VYW2F7L2-Sb8l4Vm6.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-ClnNDuqy.js → cynefinDiagram-TSTJHNR4-BrWMkhw9.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-CRMTDGPv.js → dagre-VKFMJZFB-DT5kNKEx.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-Cjt1nFI8.js → diagram-FQU43EPY-Dy0Bxyod.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DynrWO-8.js → diagram-G47NLZAW-R_yWa2-6.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-CkEgxg4T.js → diagram-NH7WQ7WH-BV2cGmgX.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DzB43wzH.js → diagram-OA4YK3LP-auayiDmq.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-DqxH3-sW.js → diagram-WEI45ONY-CE7bHIYE.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-5NxDrqIa.js → ebnfDiagram-CCIWWBDH-C3SMk6Uu.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-OfiKzPj4.js → erDiagram-Q63AITRT-CnTs6X2D.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-DsqfPM3s.js → flowDiagram-23GEKE2U-DobYm7mq.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-N48fLign.js → ganttDiagram-NO4QXBWP-AkTQtAsR.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-rn1cXRYh.js → gitGraphDiagram-IHSO6WYX-CpLWsqgx.js} +1 -1
- package/web/dist/assets/{index-BHp6FirC.js → index-BCzNnCl0.js} +6 -6
- package/web/dist/assets/index-BUp5hFdX.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CjJ3TLmy.js → infoDiagram-FWYZ7A6U-ByzTSgj_.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-DvCylaIQ.js → ishikawaDiagram-FXEZZL3T-CaL9Wi8i.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B_Ethynx.js → journeyDiagram-5HDEW3XC-BillueZJ.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-BlQuhDqn.js → kanban-definition-HUTT4EX6-DI_IRVWQ.js} +1 -1
- package/web/dist/assets/{linear-DIwvxXrN.js → linear-Cabaq-RD.js} +1 -1
- package/web/dist/assets/{mermaid.core-CfAsl9Pc.js → mermaid.core-D8KnBjxP.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BxJep4HE.js → mindmap-definition-LN4V7U3C-Bl7FpPwk.js} +1 -1
- package/web/dist/assets/{papaparse.min-BjO79nbw.js → papaparse.min-CcjjKk0h.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-yceXoFi0.js → pegDiagram-2B236MQR-CmsG2owD.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-BNBzpT5B.js → pieDiagram-ENE6RG2P-CLA_mcHl.js} +1 -1
- package/web/dist/assets/pilotswarm-DMCBXi5A.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-DCg1DTax.js → quadrantDiagram-ABIIQ3AL-hT9bnnUG.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-DQdy8xST.js → railroadDiagram-RFXS5EU6-CNC0kFW6.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-N8PYSqS8.js → requirementDiagram-TGXJPOKE-C-nSthhK.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DK9WhfTA.js → sankeyDiagram-HTMAVEWB-C5JdV5bI.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DEpALkWi.js → sequenceDiagram-DBY2YBRQ-BMKBxKk9.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKOEPe9x.js → sizeCapture-X5ZJPWSS-CbwNw-TR.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-Dwwyy0hH.js → stateDiagram-2N3HPSRC-ChvGrUa_.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DLbEwVA2.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-hDoJpKdQ.js → swimlanes-5IMT3BWC-D9Uzkbi7.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DMzhdO6y.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-CPXnQ7LH.js → timeline-definition-FHXFAJF6-ctFEfgUp.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-CYsOwGTT.js → vennDiagram-L72KCM5P-DGrJhlyF.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-DKh__oEb.js → wardleyDiagram-EHGQE667-BGqtZ8dQ.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Bws7l8k9.js → xychartDiagram-FW5EYKEG-DAXXs9xC.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +22 -0
- package/web/dist/assets/channel-DE0-pG_Z.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B0o6by6m.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B0o6by6m.js +0 -1
- package/web/dist/assets/index-DEymVbi5.css +0 -1
- package/web/dist/assets/pilotswarm-DewDyJhu.js +0 -97
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CRKYt8KU.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DtnCdOQI.js +0 -8
package/ui/core/src/reducer.js
CHANGED
|
@@ -832,6 +832,17 @@ export function appReducer(state, action) {
|
|
|
832
832
|
if (Boolean(action.enabled) === Boolean(state.ui.touchScale)) return state;
|
|
833
833
|
return { ...state, ui: { ...state.ui, touchScale: Boolean(action.enabled) } };
|
|
834
834
|
|
|
835
|
+
case "ui/agentPickerUsage": {
|
|
836
|
+
const usage = action.usage && typeof action.usage === "object" && !Array.isArray(action.usage)
|
|
837
|
+
? action.usage
|
|
838
|
+
: {};
|
|
839
|
+
return { ...state, ui: { ...state.ui, agentPickerUsage: usage } };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
case "ui/sessionDetailCollapsed":
|
|
843
|
+
if (Boolean(action.collapsed) === Boolean(state.ui.sessionDetailCollapsed)) return state;
|
|
844
|
+
return { ...state, ui: { ...state.ui, sessionDetailCollapsed: Boolean(action.collapsed) } };
|
|
845
|
+
|
|
835
846
|
case "profileSettings/apply": {
|
|
836
847
|
const settings = action.settings && typeof action.settings === "object" && !Array.isArray(action.settings)
|
|
837
848
|
? action.settings
|
|
@@ -845,6 +856,11 @@ export function appReducer(state, action) {
|
|
|
845
856
|
// say "rich" fall through to the transcript below.
|
|
846
857
|
const hasTouchScale = Object.prototype.hasOwnProperty.call(settings, "touchScale")
|
|
847
858
|
&& typeof settings.touchScale === "boolean";
|
|
859
|
+
const hasSessionDetailCollapsed = Object.prototype.hasOwnProperty.call(settings, "sessionDetailCollapsed")
|
|
860
|
+
&& typeof settings.sessionDetailCollapsed === "boolean";
|
|
861
|
+
const hasAgentPickerUsage = Object.prototype.hasOwnProperty.call(settings, "agentPickerUsage")
|
|
862
|
+
&& settings.agentPickerUsage && typeof settings.agentPickerUsage === "object"
|
|
863
|
+
&& !Array.isArray(settings.agentPickerUsage);
|
|
848
864
|
const hasRightPaneMode = Object.prototype.hasOwnProperty.call(settings, "rightPaneMode")
|
|
849
865
|
&& (settings.rightPaneMode === "canvas" || settings.rightPaneMode === "panes");
|
|
850
866
|
// The two independent columns. A profile written by a build that
|
|
@@ -921,6 +937,12 @@ export function appReducer(state, action) {
|
|
|
921
937
|
...selection.ui,
|
|
922
938
|
themeId: hasTheme ? settings.themeId.trim() : selection.ui.themeId,
|
|
923
939
|
touchScale: hasTouchScale ? settings.touchScale : selection.ui.touchScale,
|
|
940
|
+
sessionDetailCollapsed: hasSessionDetailCollapsed
|
|
941
|
+
? settings.sessionDetailCollapsed
|
|
942
|
+
: selection.ui.sessionDetailCollapsed,
|
|
943
|
+
agentPickerUsage: hasAgentPickerUsage
|
|
944
|
+
? settings.agentPickerUsage
|
|
945
|
+
: selection.ui.agentPickerUsage,
|
|
924
946
|
rightPaneMode: hasRightPaneMode ? settings.rightPaneMode : selection.ui.rightPaneMode,
|
|
925
947
|
...(nextDesktopPanes
|
|
926
948
|
? { canvasOpen: nextDesktopPanes.canvasOpen, diagnosticsOpen: nextDesktopPanes.diagnosticsOpen, canvasZen: nextDesktopPanes.zen === true }
|
|
@@ -1015,6 +1037,48 @@ export function appReducer(state, action) {
|
|
|
1015
1037
|
},
|
|
1016
1038
|
};
|
|
1017
1039
|
|
|
1040
|
+
// ── Budget screen: tab + the Cluster summary ─────────────────
|
|
1041
|
+
case "budget/tab": {
|
|
1042
|
+
const tab = action.tab === "summary" ? "summary" : "providers";
|
|
1043
|
+
if (state.budget.tab === tab) return state;
|
|
1044
|
+
return { ...state, budget: { ...state.budget, tab } };
|
|
1045
|
+
}
|
|
1046
|
+
case "budget/summary/filter": {
|
|
1047
|
+
const prev = state.budget.summary;
|
|
1048
|
+
const days = [14, 30, 90].includes(Number(action.days)) ? Number(action.days) : prev.days;
|
|
1049
|
+
const providers = Array.isArray(action.providers)
|
|
1050
|
+
? action.providers.map((p) => String(p ?? "").trim()).filter(Boolean)
|
|
1051
|
+
: prev.providers;
|
|
1052
|
+
const preset = ["all", "shared", "users", "custom"].includes(action.preset) ? action.preset
|
|
1053
|
+
: (Array.isArray(action.providers) ? "custom" : prev.preset);
|
|
1054
|
+
return { ...state, budget: { ...state.budget, summary: { ...prev, days, providers, preset } } };
|
|
1055
|
+
}
|
|
1056
|
+
case "budget/summary/loading":
|
|
1057
|
+
return { ...state, budget: { ...state.budget, summary: { ...state.budget.summary, loading: true } } };
|
|
1058
|
+
case "budget/summary/loaded":
|
|
1059
|
+
return {
|
|
1060
|
+
...state,
|
|
1061
|
+
budget: {
|
|
1062
|
+
...state.budget,
|
|
1063
|
+
summary: {
|
|
1064
|
+
...state.budget.summary,
|
|
1065
|
+
loading: false,
|
|
1066
|
+
error: null,
|
|
1067
|
+
fetchedAt: Number(action.fetchedAt) || Date.now(),
|
|
1068
|
+
data: action.data && typeof action.data === "object" ? action.data : null,
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
};
|
|
1072
|
+
case "budget/summary/failed":
|
|
1073
|
+
// Keep the last answer on screen; say the read failed beside it.
|
|
1074
|
+
return {
|
|
1075
|
+
...state,
|
|
1076
|
+
budget: {
|
|
1077
|
+
...state.budget,
|
|
1078
|
+
summary: { ...state.budget.summary, loading: false, error: String(action.error || "The summary could not be read.") },
|
|
1079
|
+
},
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1018
1082
|
case "ui/diagnosticsSplitAdjust":
|
|
1019
1083
|
return {
|
|
1020
1084
|
...state,
|
|
@@ -2458,6 +2522,29 @@ export function appReducer(state, action) {
|
|
|
2458
2522
|
},
|
|
2459
2523
|
};
|
|
2460
2524
|
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Put the banner down.
|
|
2527
|
+
*
|
|
2528
|
+
* A failed mutation writes mutation.error, which the Model Providers
|
|
2529
|
+
* pane renders as a red band. Nothing cleared it except the NEXT
|
|
2530
|
+
* mutation, so cancelling a failed sheet left the band sitting there
|
|
2531
|
+
* over an unchanged list, and it survived until something else
|
|
2532
|
+
* succeeded. The sheet that raised it owns it; when that sheet goes,
|
|
2533
|
+
* so does this.
|
|
2534
|
+
*/
|
|
2535
|
+
case "admin/modelProviders/mutationDismissed": {
|
|
2536
|
+
if (!state.admin?.modelProviders?.mutation?.error) return state;
|
|
2537
|
+
return {
|
|
2538
|
+
...state,
|
|
2539
|
+
admin: {
|
|
2540
|
+
...state.admin,
|
|
2541
|
+
modelProviders: {
|
|
2542
|
+
...state.admin.modelProviders,
|
|
2543
|
+
mutation: { ...state.admin.modelProviders.mutation, error: null },
|
|
2544
|
+
},
|
|
2545
|
+
},
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2461
2548
|
case "admin/modelProviders/select": {
|
|
2462
2549
|
return {
|
|
2463
2550
|
...state,
|
|
@@ -2495,6 +2582,7 @@ export function appReducer(state, action) {
|
|
|
2495
2582
|
}
|
|
2496
2583
|
case "admin/modelProviders/createBegin": {
|
|
2497
2584
|
const draft = String(action.name || "");
|
|
2585
|
+
const update = action.mode === "update";
|
|
2498
2586
|
return {
|
|
2499
2587
|
...state,
|
|
2500
2588
|
admin: {
|
|
@@ -2503,12 +2591,13 @@ export function appReducer(state, action) {
|
|
|
2503
2591
|
...state.admin.modelProviders,
|
|
2504
2592
|
create: {
|
|
2505
2593
|
editing: true,
|
|
2506
|
-
|
|
2507
|
-
|
|
2594
|
+
mode: update ? "update" : "create",
|
|
2595
|
+
stage: update || action.stage === "credential" ? "credential" : "name",
|
|
2596
|
+
name: update ? draft : "",
|
|
2508
2597
|
typeId: String(action.typeId || ""),
|
|
2509
2598
|
shared: action.shared === true,
|
|
2510
|
-
draft,
|
|
2511
|
-
cursorIndex: draft.length,
|
|
2599
|
+
draft: update ? "" : draft,
|
|
2600
|
+
cursorIndex: update ? 0 : draft.length,
|
|
2512
2601
|
saving: false,
|
|
2513
2602
|
error: null,
|
|
2514
2603
|
},
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -1812,6 +1812,21 @@ function prefixRuns(text, color = "gray", options = {}) {
|
|
|
1812
1812
|
// (block) chat renderer can consume the same labeling rules without the
|
|
1813
1813
|
// terminal run/prefix shape. Returns { time, glyph, glyphColor, roleLabel,
|
|
1814
1814
|
// roleColor }.
|
|
1815
|
+
/**
|
|
1816
|
+
* Is this the agent's own opening instruction rather than something a person
|
|
1817
|
+
* sent? The kickoff goes onto the queue as a user-role prompt — that is how a
|
|
1818
|
+
* turn starts — but it is stamped with a SYSTEM sender at the origin
|
|
1819
|
+
* (client.ts, createAgentSession). Anything a person actually sends carries a
|
|
1820
|
+
* `kind: "user"` sender stamped from the validated auth context.
|
|
1821
|
+
*
|
|
1822
|
+
* Sessions created before the stamp existed have no sender on that message, so
|
|
1823
|
+
* their kickoff still reads as "You". Nothing can distinguish it after the
|
|
1824
|
+
* fact; only new sessions are correct.
|
|
1825
|
+
*/
|
|
1826
|
+
export function isAgentKickoffMessage(message) {
|
|
1827
|
+
return message?.role === "user" && message?.sender?.kind === "system";
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1815
1830
|
function describeChatMessageHeader(message, options = {}) {
|
|
1816
1831
|
const time = formatTimestamp(message?.createdAt || message?.time);
|
|
1817
1832
|
// A user.message is labeled from the CURRENT VIEWER's perspective: "You"
|
|
@@ -1837,7 +1852,12 @@ function describeChatMessageHeader(message, options = {}) {
|
|
|
1837
1852
|
);
|
|
1838
1853
|
const ownerSuffix = senderIsOwner ? " (owner)" : "";
|
|
1839
1854
|
|
|
1840
|
-
const roleLabel =
|
|
1855
|
+
const roleLabel = isAgentKickoffMessage(message)
|
|
1856
|
+
// Belt and braces: the chat pane collapses a kickoff before it reaches
|
|
1857
|
+
// here, but the TUI and any other renderer share this labeling, and
|
|
1858
|
+
// "You" is the one label this message must never carry.
|
|
1859
|
+
? (sender?.display || "System")
|
|
1860
|
+
: isUser
|
|
1841
1861
|
? ((fromOtherPerson ? (sender.display || sender.subject || "User") : "You") + ownerSuffix)
|
|
1842
1862
|
: message?.role === "assistant"
|
|
1843
1863
|
? "Agent"
|
|
@@ -2440,6 +2460,22 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
|
|
|
2440
2460
|
}
|
|
2441
2461
|
}
|
|
2442
2462
|
|
|
2463
|
+
// An agent's opening instruction is plumbing, not conversation. It rides
|
|
2464
|
+
// the queue as a user-role prompt (that is how a turn starts), so without
|
|
2465
|
+
// this it rendered as a full chat message attributed to "You" — the
|
|
2466
|
+
// reader's own transcript opening with a wall of instructions they never
|
|
2467
|
+
// wrote. Collapsed to one line, openable, same affordance as a sub-agent
|
|
2468
|
+
// response.
|
|
2469
|
+
if (isAgentKickoffMessage(message)) {
|
|
2470
|
+
return [buildSystemNoticeLine({
|
|
2471
|
+
title: message?.sender?.display || "Agent kickoff",
|
|
2472
|
+
summary: "opening instruction",
|
|
2473
|
+
timestamp: formatTimestamp(message?.createdAt || message?.time),
|
|
2474
|
+
body: message?.text || "",
|
|
2475
|
+
color: "gray",
|
|
2476
|
+
})];
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2443
2479
|
if (message?.role !== "user" && message?.role !== "assistant") {
|
|
2444
2480
|
const strippedSystemText = stripLeadingRehydrationNoticeText(message?.text || "");
|
|
2445
2481
|
if (message?.role === "system" && !strippedSystemText) {
|
|
@@ -4638,6 +4674,7 @@ export function selectAdminProviderCreateModal(state, maxWidth = 76) {
|
|
|
4638
4674
|
const create = admin.modelProviders?.create || {};
|
|
4639
4675
|
if (!admin.visible || !create.editing) return null;
|
|
4640
4676
|
const credentialStage = create.stage === "credential";
|
|
4677
|
+
const updating = create.mode === "update";
|
|
4641
4678
|
const shared = create.shared === true;
|
|
4642
4679
|
const ownTitle = /github/i.test(create.typeId || "")
|
|
4643
4680
|
? "Add GitHub Copilot provider"
|
|
@@ -4645,7 +4682,9 @@ export function selectAdminProviderCreateModal(state, maxWidth = 76) {
|
|
|
4645
4682
|
const value = String(create.draft || "");
|
|
4646
4683
|
return {
|
|
4647
4684
|
type: "adminProviderCreate",
|
|
4648
|
-
title:
|
|
4685
|
+
title: updating
|
|
4686
|
+
? `Update key for ${create.name}`
|
|
4687
|
+
: credentialStage
|
|
4649
4688
|
? (shared ? "Add shared model provider" : ownTitle)
|
|
4650
4689
|
: (shared ? "Name the shared provider" : ownTitle),
|
|
4651
4690
|
label: credentialStage ? "credential" : "provider name",
|
|
@@ -4667,7 +4706,7 @@ export function selectAdminProviderCreateModal(state, maxWidth = 76) {
|
|
|
4667
4706
|
{ text: "Letters, numbers, dot, dash, and underscore only.", color: "gray" },
|
|
4668
4707
|
],
|
|
4669
4708
|
helpLines: credentialStage
|
|
4670
|
-
? ["Type/paste credential",
|
|
4709
|
+
? ["Type/paste credential", `Enter ${updating ? "update key" : "create provider"}`, "Esc cancel and clear"]
|
|
4671
4710
|
: ["Type provider name", "Tab next provider type", "Enter continue to credential", "Esc cancel"],
|
|
4672
4711
|
};
|
|
4673
4712
|
}
|
|
@@ -4790,9 +4829,30 @@ function budgetCell(raw, overall, nowMs) {
|
|
|
4790
4829
|
// At 100/100 the two numbers are the story; the case this exists for is a
|
|
4791
4830
|
// cell reading 13% of your share on a provider that has nothing left.
|
|
4792
4831
|
const blockedButLooksFine = !overall && providerExhausted && (pct == null || pct < 100);
|
|
4832
|
+
// What the used figure is made of, for the pair on screen. A limit is on
|
|
4833
|
+
// the total — input + output — and the cache figures are parts OF the
|
|
4834
|
+
// input (what was served from the cache, what was written to it), not
|
|
4835
|
+
// additions to it. The parts are the meter's own turns over the meter's
|
|
4836
|
+
// own window, so input + output is the used figure beside them.
|
|
4837
|
+
const part = (key) => {
|
|
4838
|
+
const v = overall ? raw?.[key] : raw?.[`your${key[0].toUpperCase()}${key.slice(1)}`];
|
|
4839
|
+
return v == null ? null : Number(v);
|
|
4840
|
+
};
|
|
4841
|
+
const splitInput = part("inputTokens");
|
|
4842
|
+
const split = Number.isFinite(splitInput) ? {
|
|
4843
|
+
input: splitInput,
|
|
4844
|
+
output: part("outputTokens") || 0,
|
|
4845
|
+
cacheRead: part("cacheReadTokens") || 0,
|
|
4846
|
+
cacheWrite: part("cacheWriteTokens") || 0,
|
|
4847
|
+
} : null;
|
|
4848
|
+
const splitText = split
|
|
4849
|
+
? `in ${formatCompactNumber(split.input)} (cache r ${formatCompactNumber(split.cacheRead)} · w ${formatCompactNumber(split.cacheWrite)}) · out ${formatCompactNumber(split.output)}`
|
|
4850
|
+
: null;
|
|
4793
4851
|
return {
|
|
4794
4852
|
providerExhausted: blockedButLooksFine,
|
|
4795
4853
|
ruleId: raw?.ruleId || null,
|
|
4854
|
+
split,
|
|
4855
|
+
splitText,
|
|
4796
4856
|
usedTokens: known ? usedTokens : null,
|
|
4797
4857
|
quotaTokens: Number.isFinite(quotaTokens) ? quotaTokens : null,
|
|
4798
4858
|
usedLabel,
|
|
@@ -4935,6 +4995,97 @@ function budgetTableRow(raw, { overall, selectedProvider, selectedScope = "*", n
|
|
|
4935
4995
|
* Rows are NOT sorted here. The server returns them in render order and a
|
|
4936
4996
|
* sort would move a model row out from under the provider it belongs to.
|
|
4937
4997
|
*/
|
|
4998
|
+
/**
|
|
4999
|
+
* The Cluster summary tab's view: the filter as the picker shows it, the
|
|
5000
|
+
* three KPI windows, a chart series with every day of the window present
|
|
5001
|
+
* (the database returns only days that had turns), the per-model rows with
|
|
5002
|
+
* a sparkline aligned to those same days, and the charge-class split.
|
|
5003
|
+
*/
|
|
5004
|
+
export function selectUsageSummary(state) {
|
|
5005
|
+
const budget = state.budget || {};
|
|
5006
|
+
const summary = budget.summary || {};
|
|
5007
|
+
const data = summary.data && typeof summary.data === "object" ? summary.data : null;
|
|
5008
|
+
const days = [14, 30, 90].includes(Number(summary.days)) ? Number(summary.days) : 14;
|
|
5009
|
+
|
|
5010
|
+
// Provider choices come from the grid the Providers tab already read:
|
|
5011
|
+
// one entry per provider row, with its class, so the picker can offer
|
|
5012
|
+
// Shared / Users as presets and each name as a checkbox.
|
|
5013
|
+
const providers = [];
|
|
5014
|
+
const seen = new Set();
|
|
5015
|
+
for (const row of Array.isArray(budget.grid) ? budget.grid : []) {
|
|
5016
|
+
if (row?.rowKind !== "provider" || !row?.providerName || seen.has(row.providerName)) continue;
|
|
5017
|
+
seen.add(row.providerName);
|
|
5018
|
+
providers.push({ name: row.providerName, class: row.class === "shared" ? "shared" : "personal" });
|
|
5019
|
+
}
|
|
5020
|
+
const selected = new Set(Array.isArray(summary.providers) ? summary.providers : []);
|
|
5021
|
+
const preset = summary.preset || "all";
|
|
5022
|
+
|
|
5023
|
+
// Every UTC day of the window, oldest first, ending on the server's
|
|
5024
|
+
// "today" — so a quiet day is a zero bar, not a missing one.
|
|
5025
|
+
const today = typeof data?.today === "string" ? data.today : new Date().toISOString().slice(0, 10);
|
|
5026
|
+
const dayKeys = [];
|
|
5027
|
+
{
|
|
5028
|
+
const end = new Date(`${today}T00:00:00Z`).getTime();
|
|
5029
|
+
for (let i = days - 1; i >= 0; i -= 1) dayKeys.push(new Date(end - i * 86_400_000).toISOString().slice(0, 10));
|
|
5030
|
+
}
|
|
5031
|
+
const byDay = new Map((Array.isArray(data?.daily) ? data.daily : []).map((d) => [d.day, d]));
|
|
5032
|
+
const series = dayKeys.map((day) => {
|
|
5033
|
+
const d = byDay.get(day) || {};
|
|
5034
|
+
return {
|
|
5035
|
+
day,
|
|
5036
|
+
input: Number(d.input) || 0,
|
|
5037
|
+
output: Number(d.output) || 0,
|
|
5038
|
+
cacheRead: Number(d.cacheRead) || 0,
|
|
5039
|
+
cacheWrite: Number(d.cacheWrite) || 0,
|
|
5040
|
+
total: Number(d.total) || 0,
|
|
5041
|
+
turns: Number(d.turns) || 0,
|
|
5042
|
+
};
|
|
5043
|
+
});
|
|
5044
|
+
const windowTotal = series.reduce((sum, d) => sum + d.total, 0);
|
|
5045
|
+
const models = (Array.isArray(data?.models) ? data.models : []).map((m) => {
|
|
5046
|
+
const spark = new Map((Array.isArray(m.daily) ? m.daily : []).map((d) => [d.day, Number(d.total) || 0]));
|
|
5047
|
+
const total = Number(m.total) || 0;
|
|
5048
|
+
return {
|
|
5049
|
+
model: String(m.model || "?"),
|
|
5050
|
+
providers: Number(m.providers) || 0,
|
|
5051
|
+
turns: Number(m.turns) || 0,
|
|
5052
|
+
input: Number(m.input) || 0,
|
|
5053
|
+
output: Number(m.output) || 0,
|
|
5054
|
+
cacheRead: Number(m.cacheRead) || 0,
|
|
5055
|
+
cacheWrite: Number(m.cacheWrite) || 0,
|
|
5056
|
+
total,
|
|
5057
|
+
share: windowTotal > 0 ? total / windowTotal : 0,
|
|
5058
|
+
sparkline: dayKeys.map((day) => spark.get(day) || 0),
|
|
5059
|
+
};
|
|
5060
|
+
});
|
|
5061
|
+
const win = (key) => {
|
|
5062
|
+
const w = data?.windows?.[key] || {};
|
|
5063
|
+
return {
|
|
5064
|
+
input: Number(w.input) || 0, output: Number(w.output) || 0,
|
|
5065
|
+
cacheRead: Number(w.cacheRead) || 0, cacheWrite: Number(w.cacheWrite) || 0,
|
|
5066
|
+
total: Number(w.total) || 0, turns: Number(w.turns) || 0, sessions: Number(w.sessions) || 0,
|
|
5067
|
+
};
|
|
5068
|
+
};
|
|
5069
|
+
return {
|
|
5070
|
+
loading: Boolean(summary.loading),
|
|
5071
|
+
error: summary.error || null,
|
|
5072
|
+
loaded: Boolean(data),
|
|
5073
|
+
scope: data?.scope === "mine" ? "mine" : "cluster",
|
|
5074
|
+
days,
|
|
5075
|
+
today,
|
|
5076
|
+
preset,
|
|
5077
|
+
selectedProviders: Array.from(selected),
|
|
5078
|
+
providers,
|
|
5079
|
+
windows: { day: win("day"), week: win("week"), month: win("month") },
|
|
5080
|
+
series,
|
|
5081
|
+
windowTotal,
|
|
5082
|
+
models,
|
|
5083
|
+
classes: (Array.isArray(data?.classes) ? data.classes : []).map((c) => ({
|
|
5084
|
+
chargeClass: String(c.chargeClass || "?"), total: Number(c.total) || 0, turns: Number(c.turns) || 0,
|
|
5085
|
+
})),
|
|
5086
|
+
};
|
|
5087
|
+
}
|
|
5088
|
+
|
|
4938
5089
|
export function selectProviderTable(state) {
|
|
4939
5090
|
const budget = state.budget || {};
|
|
4940
5091
|
const nowMs = Date.now();
|
|
@@ -5227,16 +5378,17 @@ export function selectStatusBar(state) {
|
|
|
5227
5378
|
}
|
|
5228
5379
|
if (state.ui.modal?.type === "themePicker") {
|
|
5229
5380
|
return {
|
|
5230
|
-
left: "Select a
|
|
5381
|
+
left: "Select a theme",
|
|
5231
5382
|
right: "up/down move · enter apply · esc close",
|
|
5232
5383
|
};
|
|
5233
5384
|
}
|
|
5234
5385
|
if (state.ui.modal?.type === "sessionAgentPicker") {
|
|
5235
5386
|
return {
|
|
5236
5387
|
left: "Select an agent for the new session",
|
|
5237
|
-
//
|
|
5238
|
-
//
|
|
5239
|
-
|
|
5388
|
+
// Every row is an agent now, so Enter always starts one. The
|
|
5389
|
+
// arrows are no longer named because they no longer do anything
|
|
5390
|
+
// here — they belong to the search box, where they move the caret.
|
|
5391
|
+
right: "type to search · up/down move · enter start · esc cancel",
|
|
5240
5392
|
};
|
|
5241
5393
|
}
|
|
5242
5394
|
if (state.ui.modal?.type === "sessionGroupPicker") {
|
|
@@ -6503,15 +6655,23 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
6503
6655
|
], contentWidth);
|
|
6504
6656
|
} else {
|
|
6505
6657
|
const callable = item?.kind === "generic" || item?.supportsDirectStart !== false;
|
|
6506
|
-
//
|
|
6507
|
-
//
|
|
6508
|
-
//
|
|
6509
|
-
//
|
|
6510
|
-
|
|
6511
|
-
const
|
|
6658
|
+
// ONE LINE PER AGENT. The package is trailing metadata rather than a
|
|
6659
|
+
// row of its own, so the list is as long as the number of agents —
|
|
6660
|
+
// the grouped shape spent a second row on every single-agent package
|
|
6661
|
+
// saying the same thing twice.
|
|
6662
|
+
const glyph = item?.kind === "generic" ? "○ " : "★ ";
|
|
6663
|
+
const name = item?.title || item?.agentName || item?.id || "Agent";
|
|
6664
|
+
const pkg = item?.kind === "generic" ? "" : (item?.packageTitle || item?.packageName || "");
|
|
6665
|
+
// Only shown while sorting BY use, where the number is the sort key
|
|
6666
|
+
// and therefore explains the order. Everywhere else it is noise.
|
|
6667
|
+
const uses = modal.sort === "used" && Number(item?.usageCount) > 0
|
|
6668
|
+
? ` ${item.usageCount}×`
|
|
6669
|
+
: "";
|
|
6512
6670
|
labelRuns = fitRuns([
|
|
6513
6671
|
{ text: `${indent}${glyph}`, color: callable ? "yellow" : "gray" },
|
|
6514
|
-
{ text:
|
|
6672
|
+
{ text: name, color: callable ? "white" : "gray", bold: callable },
|
|
6673
|
+
...(pkg ? [{ text: ` ${pkg}`, color: "gray" }] : []),
|
|
6674
|
+
...(uses ? [{ text: uses, color: "cyan" }] : []),
|
|
6515
6675
|
], contentWidth);
|
|
6516
6676
|
}
|
|
6517
6677
|
|
|
@@ -8570,7 +8730,7 @@ export function selectThemePickerModal(state, maxWidth = 80) {
|
|
|
8570
8730
|
[{
|
|
8571
8731
|
text: previewingTheme
|
|
8572
8732
|
? `Previewing now. Cancel reverts to ${originalTheme?.label || "the previous theme"}.`
|
|
8573
|
-
: "Currently active
|
|
8733
|
+
: "Currently active.",
|
|
8574
8734
|
color: previewingTheme ? "yellow" : "green",
|
|
8575
8735
|
}],
|
|
8576
8736
|
buildThemeSwatchRuns([
|
package/ui/core/src/state.js
CHANGED
|
@@ -359,6 +359,17 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
359
359
|
// portrait is not the only case — a wall display or a shared screen wants
|
|
360
360
|
// the same thing — so it is a preference, not a viewport query.
|
|
361
361
|
touchScale: false,
|
|
362
|
+
// The session detail box starts folded to its one-line summary. It
|
|
363
|
+
// is ten rows of reference detail against a list you are trying to
|
|
364
|
+
// read, and most of it is looked up once per session, not per
|
|
365
|
+
// glance. Expanding is one click and the choice persists.
|
|
366
|
+
sessionDetailCollapsed: true,
|
|
367
|
+
// Per-person agent start counts, {agentName: n}. Drives the
|
|
368
|
+
// picker's default "Most used" sort. State-owned so every writer
|
|
369
|
+
// of the profile document sees the same value at the same moment;
|
|
370
|
+
// a preserved ref refreshed by a 5s poll lost the count to the
|
|
371
|
+
// very next save.
|
|
372
|
+
agentPickerUsage: {},
|
|
362
373
|
modal: null,
|
|
363
374
|
fullscreenPane: null,
|
|
364
375
|
layout: {
|
|
@@ -519,6 +530,26 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
519
530
|
// why on the session row too, not only on this screen. See
|
|
520
531
|
// docs/proposals/providers-and-budgets-meters.md.
|
|
521
532
|
budget: {
|
|
533
|
+
// Which tab of the Budget screen is up: the per-provider table
|
|
534
|
+
// ("providers") or the cluster summary ("summary").
|
|
535
|
+
tab: "providers",
|
|
536
|
+
// The Cluster summary tab: totals for today / week / month, the
|
|
537
|
+
// per-day series and the per-model pivot, over a provider filter.
|
|
538
|
+
summary: {
|
|
539
|
+
loading: false,
|
|
540
|
+
error: null,
|
|
541
|
+
fetchedAt: 0,
|
|
542
|
+
// 14, 30 or 90 — the chart's and the model table's window.
|
|
543
|
+
days: 14,
|
|
544
|
+
// Provider filter. `preset` is what the picker shows as the
|
|
545
|
+
// headline choice; `providers` is the exact list the request
|
|
546
|
+
// carried (empty = every provider). A custom pick clears the
|
|
547
|
+
// preset.
|
|
548
|
+
preset: "all",
|
|
549
|
+
providers: [],
|
|
550
|
+
// The last answer, verbatim from getProviderUsageSummary.
|
|
551
|
+
data: null,
|
|
552
|
+
},
|
|
522
553
|
// A first load, with nothing on screen yet.
|
|
523
554
|
loading: false,
|
|
524
555
|
// A re-read WITH numbers already on screen. Separate from
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Column layout for fit-width markdown / chat tables. Lives in ui-core so
|
|
2
|
+
// the algorithm can be unit-tested without a DOM; web-app.js renders the
|
|
3
|
+
// result as a <colgroup> of percentages plus a min-width in ch.
|
|
4
|
+
//
|
|
5
|
+
// Terminology: the FLEX column is the one prose-like column that wraps to
|
|
6
|
+
// absorb overflow; every other column is RIGID and keeps its content width.
|
|
7
|
+
|
|
8
|
+
export function normalizeTableCellText(value = "") {
|
|
9
|
+
return String(value || "")
|
|
10
|
+
.replace(/\s+/g, " ")
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FIT_WIDTH_FLEX_HEADER_KEYWORDS = [
|
|
15
|
+
"description",
|
|
16
|
+
"mechanism",
|
|
17
|
+
"details",
|
|
18
|
+
"notes",
|
|
19
|
+
"summary",
|
|
20
|
+
"message",
|
|
21
|
+
"comment",
|
|
22
|
+
"reason",
|
|
23
|
+
"body",
|
|
24
|
+
"content",
|
|
25
|
+
"explanation",
|
|
26
|
+
"rationale",
|
|
27
|
+
"one-liner",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const FIT_WIDTH_FLEX_MIN_MAX_LEN = 24;
|
|
31
|
+
const FIT_WIDTH_RIGID_CHAR_CAP = 32;
|
|
32
|
+
const FIT_WIDTH_MIN_RIGID_CHARS = 6;
|
|
33
|
+
const FIT_WIDTH_FLEX_MIN_CHARS = 30;
|
|
34
|
+
const FIT_WIDTH_FLEX_MAX_CHARS = 56;
|
|
35
|
+
const FIT_WIDTH_FLEX_MIN_FRACTION = 0.4;
|
|
36
|
+
const FIT_WIDTH_FLEX_TO_RIGID_RATIO = 1.4;
|
|
37
|
+
const FIT_WIDTH_MIN_EXTRA_CHARS = 2;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute per-column layout for a fit-width markdown / chat table.
|
|
41
|
+
*
|
|
42
|
+
* Strategy:
|
|
43
|
+
* 1. Measure max cell length and "wrappability" (cells with whitespace) per column.
|
|
44
|
+
* 2. Identify a single "flex" column — long, prose-like, ideally with a
|
|
45
|
+
* header keyword like Description / Mechanism / Notes. This column
|
|
46
|
+
* absorbs overflow by wrapping aggressively.
|
|
47
|
+
* 3. Give every other column a budget = clamp(maxLen + padding, MIN, RIGID_CAP),
|
|
48
|
+
* so rigid columns stay at their content-fit width even when one
|
|
49
|
+
* sibling column has hundreds of characters of prose.
|
|
50
|
+
* 4. Give the flex column a bounded share of the rigid-column budget. A
|
|
51
|
+
* very long cell should wrap; it should not steal so much percentage
|
|
52
|
+
* width that compact columns like Count / Status collapse on phones.
|
|
53
|
+
* 5. Return a minimum table width in ch so the wrapper can scroll
|
|
54
|
+
* horizontally rather than forcing all columns below readable size.
|
|
55
|
+
*
|
|
56
|
+
* Returns { widths: ["12.34%", ...], flexIndex: number, minWidth: "64ch" }
|
|
57
|
+
* when a flex column is identified — the table renderer then forces
|
|
58
|
+
* table-layout: fixed, adds an `is-flex-column` class to the chosen column,
|
|
59
|
+
* and gives the table a readable minimum width. Returns null when no flex
|
|
60
|
+
* column is found, in which case the renderer falls back to the browser's
|
|
61
|
+
* auto-table-layout (which is already good for short / uniform tables).
|
|
62
|
+
*
|
|
63
|
+
* Background: the previous behavior used the browser's auto-table-layout
|
|
64
|
+
* unconditionally. That works well when columns are uniform but is biased
|
|
65
|
+
* toward wide columns when one column has prose hundreds of characters
|
|
66
|
+
* long (e.g. a Mechanism / Description column) — auto-layout distributes
|
|
67
|
+
* width proportional to (max-content − min-content), which lets the prose
|
|
68
|
+
* column squeeze the rigid columns down to a few characters each.
|
|
69
|
+
*/
|
|
70
|
+
export function computeFitWidthColumnLayout(rows = []) {
|
|
71
|
+
const columnCount = Math.max(0, ...rows.map((row) => row.length));
|
|
72
|
+
if (columnCount <= 0) return null;
|
|
73
|
+
|
|
74
|
+
const headerRow = rows[0] || [];
|
|
75
|
+
const dataRowCount = Math.max(1, rows.length - 1);
|
|
76
|
+
|
|
77
|
+
const stats = Array.from({ length: columnCount }, () => ({
|
|
78
|
+
max: 0,
|
|
79
|
+
// Longest run without whitespace. A rigid column wraps at spaces
|
|
80
|
+
// only, so this is the narrowest it can be drawn without breaking
|
|
81
|
+
// a word — a 60-char CamelCase test name, a URL, a hash.
|
|
82
|
+
longestToken: 0,
|
|
83
|
+
spaceCells: 0,
|
|
84
|
+
}));
|
|
85
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
86
|
+
const row = rows[rowIndex];
|
|
87
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
88
|
+
const text = normalizeTableCellText(row[columnIndex] || "");
|
|
89
|
+
if (text.length > stats[columnIndex].max) stats[columnIndex].max = text.length;
|
|
90
|
+
if (rowIndex > 0 && /\s/.test(text)) stats[columnIndex].spaceCells += 1;
|
|
91
|
+
for (const token of text.split(/\s+/)) {
|
|
92
|
+
if (token.length > stats[columnIndex].longestToken) stats[columnIndex].longestToken = token.length;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let flexIndex = -1;
|
|
98
|
+
let flexScore = 0;
|
|
99
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
100
|
+
const stat = stats[columnIndex];
|
|
101
|
+
if (stat.max < FIT_WIDTH_FLEX_MIN_MAX_LEN) continue;
|
|
102
|
+
const spaceRatio = stat.spaceCells / dataRowCount;
|
|
103
|
+
if (spaceRatio < 0.4) continue;
|
|
104
|
+
const headerText = normalizeTableCellText(headerRow[columnIndex] || "").toLowerCase();
|
|
105
|
+
const headerBonus = FIT_WIDTH_FLEX_HEADER_KEYWORDS.some((keyword) => headerText.includes(keyword)) ? 60 : 0;
|
|
106
|
+
const score = stat.max + headerBonus;
|
|
107
|
+
if (score > flexScore) {
|
|
108
|
+
flexScore = score;
|
|
109
|
+
flexIndex = columnIndex;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (flexIndex < 0) return null;
|
|
114
|
+
|
|
115
|
+
const rigidBudgets = stats.map((stat, columnIndex) => {
|
|
116
|
+
if (columnIndex === flexIndex) return 0;
|
|
117
|
+
// The cap keeps a column of many short words from hogging width; it
|
|
118
|
+
// must never cut a single word. Under the fixed layout a rigid cell
|
|
119
|
+
// wraps at spaces only, so a budget below the longest token painted
|
|
120
|
+
// that token across the next column. The floor makes the table's
|
|
121
|
+
// min-width grow instead — past the pane, the wrapper scrolls.
|
|
122
|
+
return Math.max(
|
|
123
|
+
FIT_WIDTH_MIN_RIGID_CHARS,
|
|
124
|
+
Math.min(stat.max + 2, FIT_WIDTH_RIGID_CHAR_CAP),
|
|
125
|
+
stat.longestToken + 2,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
const sumRigid = rigidBudgets.reduce((sum, value) => sum + value, 0);
|
|
129
|
+
const flexStat = stats[flexIndex];
|
|
130
|
+
const flexBudget = Math.max(
|
|
131
|
+
FIT_WIDTH_FLEX_MIN_CHARS,
|
|
132
|
+
Math.min(
|
|
133
|
+
flexStat.max * 0.45,
|
|
134
|
+
FIT_WIDTH_FLEX_MAX_CHARS,
|
|
135
|
+
Math.max(FIT_WIDTH_FLEX_MIN_CHARS, sumRigid * FIT_WIDTH_FLEX_TO_RIGID_RATIO),
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
const budgets = rigidBudgets.map((value, columnIndex) => (
|
|
139
|
+
columnIndex === flexIndex ? flexBudget : value
|
|
140
|
+
));
|
|
141
|
+
|
|
142
|
+
const totalBudget = budgets.reduce((sum, value) => sum + value, 0);
|
|
143
|
+
if (totalBudget > 0 && budgets[flexIndex] / totalBudget < FIT_WIDTH_FLEX_MIN_FRACTION) {
|
|
144
|
+
const sumRigid = totalBudget - budgets[flexIndex];
|
|
145
|
+
budgets[flexIndex] = sumRigid * (FIT_WIDTH_FLEX_MIN_FRACTION / (1 - FIT_WIDTH_FLEX_MIN_FRACTION));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const finalTotal = budgets.reduce((sum, value) => sum + value, 0);
|
|
149
|
+
if (!(finalTotal > 0)) return null;
|
|
150
|
+
return {
|
|
151
|
+
widths: budgets.map((value) => `${((value / finalTotal) * 100).toFixed(2)}%`),
|
|
152
|
+
flexIndex,
|
|
153
|
+
minWidth: `${Math.ceil(finalTotal + FIT_WIDTH_MIN_EXTRA_CHARS)}ch`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -2152,6 +2152,7 @@ function buildAdminModelProviderLines(view) {
|
|
|
2152
2152
|
if (providers.page !== "shared") {
|
|
2153
2153
|
lines.push([
|
|
2154
2154
|
{ text: " e ", color: "green", bold: true }, { text: "add ", color: "gray" },
|
|
2155
|
+
{ text: "U ", color: "yellow", bold: true }, { text: "update key ", color: "gray" },
|
|
2155
2156
|
{ text: "d ", color: "red", bold: true }, { text: "delete ", color: "gray" },
|
|
2156
2157
|
{ text: "u ", color: "cyan", bold: true }, { text: "my default ", color: "gray" },
|
|
2157
2158
|
...(view.isAdmin ? [{ text: "t ", color: "yellow", bold: true }, { text: "system use ", color: "gray" }] : []),
|