pilotswarm 0.5.44 → 0.5.46
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 +11 -0
- package/mcp/dist/src/context.d.ts.map +1 -1
- package/mcp/dist/src/context.js +9 -1
- package/mcp/dist/src/context.js.map +1 -1
- package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -1
- package/mcp/dist/src/tools/agent-packages.js +31 -0
- package/mcp/dist/src/tools/agent-packages.js.map +1 -1
- package/mcp/dist/src/util/respond.d.ts.map +1 -1
- package/mcp/dist/src/util/respond.js +6 -0
- package/mcp/dist/src/util/respond.js.map +1 -1
- package/package.json +2 -2
- package/tui/src/agents-cli.js +35 -0
- package/tui/src/node-sdk-transport.js +54 -0
- package/ui/core/src/controller.js +48 -6
- package/ui/core/src/history.js +28 -10
- package/ui/core/src/selectors.js +38 -5
- package/ui/react/src/web-app.js +327 -24
- package/web/api/canvas-plane.js +18 -6
- package/web/api/ws.js +22 -2
- package/web/auth/index.js +8 -1
- package/web/auth/normalize/entra.js +5 -1
- package/web/dist/assets/{abnfDiagram-VRR7QNED-67Q0WWge.js → abnfDiagram-VRR7QNED-GXM5pEfT.js} +1 -1
- package/web/dist/assets/{arc-BvcYFngS.js → arc-D52-xt6J.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CPBN0rk8.js → architectureDiagram-ZJ3FMSHR-Bxh49GyJ.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-D_hp1Oah.js → blockDiagram-677ZJIJ3-D8LSE94K.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DAOVGMIW.js → c4Diagram-LMCZKHZV-Dcs1NVIx.js} +1 -1
- package/web/dist/assets/channel-DE0-pG_Z.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-DajZuaot.js → chunk-2Q5K7J3B-Dy3zu7gI.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C6pXBsgt.js → chunk-32BRIVSS-5v78VX3T.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-780edrmi.js → chunk-5VM5RSS4-DUJMCfdj.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-C0e4NVuz.js → chunk-EX3LRPZG-DYyT1So9.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-uRe83I6C.js → chunk-JWPE2WC7-Buzzx5R2.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-B0WPoPjh.js → chunk-MOJQB5TN-B8fOWRD7.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-eRFuHHyG.js → chunk-RYQCIY6F-Bp04YO26.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-C8FDXprk.js → chunk-V7JOEXUC-Gw6h8gPE.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DPFpLQdB.js → chunk-VR4S4FIN-ASf4dPE-.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-CoGQOPKW.js → chunk-XXDRQBXY-C4B-MNNk.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B0o6by6m.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B0o6by6m.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BTBSfO5v.js → cose-bilkent-JH36ORCC-C0S415ey.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DgTkQEoK.js → cynefin-VYW2F7L2-LWUlL1XB.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-DiBMlKoP.js → cynefinDiagram-TSTJHNR4-ClnNDuqy.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-Qv-4dcbk.js → dagre-VKFMJZFB-CRMTDGPv.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-CgyAb7yx.js → diagram-FQU43EPY-Cjt1nFI8.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-BxN-KcGp.js → diagram-G47NLZAW-DynrWO-8.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-BiellpWz.js → diagram-NH7WQ7WH-CkEgxg4T.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-C8Ktgrwv.js → diagram-OA4YK3LP-DzB43wzH.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Bj5K1X91.js → diagram-WEI45ONY-DqxH3-sW.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DNQI2XT9.js → ebnfDiagram-CCIWWBDH-5NxDrqIa.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-D5Xkllu6.js → erDiagram-Q63AITRT-OfiKzPj4.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-DJpTtMuj.js → flowDiagram-23GEKE2U-DsqfPM3s.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-C-EQlCJf.js → ganttDiagram-NO4QXBWP-N48fLign.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C8vpSbvW.js → gitGraphDiagram-IHSO6WYX-rn1cXRYh.js} +1 -1
- package/web/dist/assets/index-BHp6FirC.js +24 -0
- package/web/dist/assets/index-DEymVbi5.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CKNzsR3r.js → infoDiagram-FWYZ7A6U-CjJ3TLmy.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CjyQq3lf.js → ishikawaDiagram-FXEZZL3T-DvCylaIQ.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-CK3EEkJt.js → journeyDiagram-5HDEW3XC-B_Ethynx.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CHQ8YkPx.js → kanban-definition-HUTT4EX6-BlQuhDqn.js} +1 -1
- package/web/dist/assets/{linear-CUYirs1h.js → linear-DIwvxXrN.js} +1 -1
- package/web/dist/assets/{mermaid.core-Di8sQC4g.js → mermaid.core-CfAsl9Pc.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-D38dmHP5.js → mindmap-definition-LN4V7U3C-BxJep4HE.js} +1 -1
- package/web/dist/assets/{papaparse.min-DWBU_pXS.js → papaparse.min-BjO79nbw.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BU759QCo.js → pegDiagram-2B236MQR-yceXoFi0.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-BbpfHbsj.js → pieDiagram-ENE6RG2P-BNBzpT5B.js} +1 -1
- package/web/dist/assets/pilotswarm-DewDyJhu.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BYZ3XHZn.js → quadrantDiagram-ABIIQ3AL-DCg1DTax.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-D18gkZBJ.js → railroadDiagram-RFXS5EU6-DQdy8xST.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-D6vo26Ib.js → requirementDiagram-TGXJPOKE-N8PYSqS8.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CxQwXnRE.js → sankeyDiagram-HTMAVEWB-DK9WhfTA.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CfGisw4A.js → sequenceDiagram-DBY2YBRQ-DEpALkWi.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-BDK7-yA-.js → sizeCapture-X5ZJPWSS-CKOEPe9x.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-8IyidYSV.js → stateDiagram-2N3HPSRC-Dwwyy0hH.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CRKYt8KU.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-CgNqXCvM.js → swimlanes-5IMT3BWC-hDoJpKdQ.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DtnCdOQI.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DAJsGj6d.js → timeline-definition-FHXFAJF6-CPXnQ7LH.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-DDVdvG5k.js → vennDiagram-L72KCM5P-CYsOwGTT.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CiAs_m72.js → wardleyDiagram-EHGQE667-DKh__oEb.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-D6OLJtsD.js → xychartDiagram-FW5EYKEG-Bws7l8k9.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +180 -6
- package/web/server.js +22 -0
- package/web/dist/assets/channel-DiW7lUT8.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CpAhBd5x.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CpAhBd5x.js +0 -1
- package/web/dist/assets/index-C2ioXNJJ.js +0 -24
- package/web/dist/assets/index-CVYm02UI.css +0 -1
- package/web/dist/assets/pilotswarm-DrM3pXOV.js +0 -97
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-F7qYkvpr.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CUKrHLkY.js +0 -8
package/ui/core/src/selectors.js
CHANGED
|
@@ -2950,16 +2950,28 @@ export function selectCanvasView(state) {
|
|
|
2950
2950
|
}
|
|
2951
2951
|
|
|
2952
2952
|
/**
|
|
2953
|
-
* A validated canvas response on its way to the agent. Flagged
|
|
2954
|
-
*
|
|
2955
|
-
*
|
|
2956
|
-
*
|
|
2953
|
+
* A validated canvas response on its way to the agent. Flagged `canvasAction`
|
|
2954
|
+
* so each host renders it its own way: the TUI prints the compact dim line
|
|
2955
|
+
* below, while the portal collapses it to a single row that opens to show the
|
|
2956
|
+
* payload. Both need the same facts, so the action name and the full payload
|
|
2957
|
+
* ride on the line rather than only inside the truncated runs.
|
|
2957
2958
|
*/
|
|
2958
2959
|
function buildCanvasActionLine(message) {
|
|
2959
2960
|
const dataKeys = message?.data && typeof message.data === "object" ? Object.keys(message.data) : [];
|
|
2960
2961
|
const detail = dataKeys.length === 1 && typeof message.data[dataKeys[0]] === "string"
|
|
2961
2962
|
? message.data[dataKeys[0]].slice(0, 120)
|
|
2962
2963
|
: (dataKeys.length ? JSON.stringify(message.data).slice(0, 120) : "");
|
|
2964
|
+
let payload = "";
|
|
2965
|
+
if (dataKeys.length) {
|
|
2966
|
+
// A page can post anything that survives structured clone; JSON.stringify
|
|
2967
|
+
// throws on a cycle or a BigInt, and this runs inside the transcript
|
|
2968
|
+
// render path where a throw blanks the whole conversation.
|
|
2969
|
+
try {
|
|
2970
|
+
payload = JSON.stringify(message.data, null, 2);
|
|
2971
|
+
} catch {
|
|
2972
|
+
payload = "";
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2963
2975
|
return {
|
|
2964
2976
|
runs: [
|
|
2965
2977
|
{ text: "[canvas] ", color: "cyan", bold: true },
|
|
@@ -2967,6 +2979,10 @@ function buildCanvasActionLine(message) {
|
|
|
2967
2979
|
...(detail ? [{ text: ` — ${detail}`, color: "gray" }] : []),
|
|
2968
2980
|
],
|
|
2969
2981
|
canvasAction: true,
|
|
2982
|
+
canvasActionName: message?.action || "action",
|
|
2983
|
+
canvasActionDetail: detail,
|
|
2984
|
+
canvasActionPayload: payload,
|
|
2985
|
+
canvasActionTime: message?.time || "",
|
|
2970
2986
|
};
|
|
2971
2987
|
}
|
|
2972
2988
|
|
|
@@ -4005,6 +4021,11 @@ export function selectAdminConsole(state) {
|
|
|
4005
4021
|
&& pkg.owner.provider === principal.provider
|
|
4006
4022
|
&& pkg.owner.subject === principal.subject),
|
|
4007
4023
|
);
|
|
4024
|
+
// Editor-level: may change contents and rollout (publish, pin,
|
|
4025
|
+
// enable/disable). The server computes `canEdit` per viewer (admin, owner
|
|
4026
|
+
// or a granted editor); ownsPackage stays the owner-only gate for scope,
|
|
4027
|
+
// delete and the editor list.
|
|
4028
|
+
const canEditPackage = (pkg) => ownsPackage(pkg) || Boolean(pkg?.canEdit);
|
|
4008
4029
|
// Built once per render, not per row.
|
|
4009
4030
|
const ownerDirectory = ownerDirectoryFromSessions(state?.sessions?.byId);
|
|
4010
4031
|
// Same rule as the session list: the owner chip only earns its place when
|
|
@@ -4046,7 +4067,7 @@ export function selectAdminConsole(state) {
|
|
|
4046
4067
|
semver: pkg.active?.semver || null,
|
|
4047
4068
|
sha7: pkg.active?.sha256 ? String(pkg.active.sha256).slice(0, 7) : null,
|
|
4048
4069
|
agentCount: Array.isArray(pkg.active?.manifest?.agents) ? pkg.active.manifest.agents.length : 0,
|
|
4049
|
-
canManage:
|
|
4070
|
+
canManage: canEditPackage(pkg),
|
|
4050
4071
|
// A user-scope package belongs to a PERSON, so it carries the same
|
|
4051
4072
|
// owner-initials chip the session rows use. Shared packages belong to
|
|
4052
4073
|
// the deployment and keep the scope badge instead.
|
|
@@ -4157,13 +4178,25 @@ export function selectAdminConsole(state) {
|
|
|
4157
4178
|
&& installedRowFor(worker)?.status === "ok").length
|
|
4158
4179
|
: 0;
|
|
4159
4180
|
const canManage = detail ? ownsPackage(detail) : (summary ? ownsPackage(summary) : false);
|
|
4181
|
+
const canEdit = detail ? canEditPackage(detail) : (summary ? canEditPackage(summary) : false);
|
|
4160
4182
|
packageDetail = {
|
|
4161
4183
|
name: pkgState.selectedName,
|
|
4162
4184
|
loading: Boolean(pkgState.detailLoading),
|
|
4163
4185
|
error: pkgState.detailError || null,
|
|
4164
4186
|
scope: (detail?.scope || summary?.scope) === "shared" ? "shared" : "user",
|
|
4165
4187
|
enabled: detail ? Boolean(detail.enabled) : Boolean(summary?.enabled),
|
|
4188
|
+
// canManage = owner/admin (scope, delete, editors);
|
|
4189
|
+
// canEdit = canManage OR granted editor (publish, pin, enable).
|
|
4166
4190
|
canManage,
|
|
4191
|
+
canEdit,
|
|
4192
|
+
editors: Array.isArray(detail?.editors)
|
|
4193
|
+
? detail.editors.map((e) => ({
|
|
4194
|
+
provider: e.provider,
|
|
4195
|
+
subject: e.subject,
|
|
4196
|
+
label: e.displayName || e.email || e.subject,
|
|
4197
|
+
grantedByDisplay: e.grantedByDisplay || null,
|
|
4198
|
+
}))
|
|
4199
|
+
: [],
|
|
4167
4200
|
createdBy: detail?.createdBy || summary?.createdBy || null,
|
|
4168
4201
|
createdAtText: adminPkgDate(detail?.createdAt || summary?.createdAt),
|
|
4169
4202
|
description: typeof manifest.description === "string" ? manifest.description : "",
|
package/ui/react/src/web-app.js
CHANGED
|
@@ -1257,6 +1257,33 @@ function SystemNoticeLine({ line, theme }) {
|
|
|
1257
1257
|
: null);
|
|
1258
1258
|
}
|
|
1259
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* A canvas action is a button press inside the drawn page, not something the
|
|
1262
|
+
* viewer typed. Printing the raw `[canvas-action] {…}` JSON as a chat message
|
|
1263
|
+
* buries the conversation in plumbing, so it renders as one collapsed row —
|
|
1264
|
+
* the same affordance as a system notice — that opens to show the exact
|
|
1265
|
+
* payload the page sent.
|
|
1266
|
+
*/
|
|
1267
|
+
function CanvasActionLine({ line, theme }) {
|
|
1268
|
+
const payload = String(line?.canvasActionPayload || "").trim();
|
|
1269
|
+
const name = line?.canvasActionName || "action";
|
|
1270
|
+
const detail = String(line?.canvasActionDetail || "").trim();
|
|
1271
|
+
const time = String(line?.canvasActionTime || "").trim();
|
|
1272
|
+
return React.createElement("details", { className: "ps-system-notice ps-canvas-action" },
|
|
1273
|
+
React.createElement("summary", {
|
|
1274
|
+
className: "ps-system-notice-summary ps-canvas-action-summary",
|
|
1275
|
+
style: { color: resolveColor(theme, "gray") || "var(--ps-muted)" },
|
|
1276
|
+
},
|
|
1277
|
+
React.createElement("span", { className: "ps-canvas-action-tag" }, "canvas"),
|
|
1278
|
+
React.createElement("span", { className: "ps-system-notice-summary-text" },
|
|
1279
|
+
`${name}${detail ? ` — ${detail}` : ""}`),
|
|
1280
|
+
time ? React.createElement("span", { className: "ps-canvas-action-time" }, time) : null),
|
|
1281
|
+
payload
|
|
1282
|
+
? React.createElement("div", { className: "ps-system-notice-body" },
|
|
1283
|
+
React.createElement("pre", { className: "ps-canvas-action-payload" }, payload))
|
|
1284
|
+
: null);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1260
1287
|
/**
|
|
1261
1288
|
* Coalesce high-frequency dispatches to one per animation frame.
|
|
1262
1289
|
*
|
|
@@ -1303,6 +1330,11 @@ const Line = React.memo(function Line({ line, theme, className = "" }) {
|
|
|
1303
1330
|
if (line.kind === "systemNotice") {
|
|
1304
1331
|
return React.createElement(SystemNoticeLine, { line, theme });
|
|
1305
1332
|
}
|
|
1333
|
+
// Checked before `kind`: the canvas-action line carries runs for the TUI
|
|
1334
|
+
// but the portal renders it as its own collapsed row, not as those runs.
|
|
1335
|
+
if (line.canvasAction) {
|
|
1336
|
+
return React.createElement(CanvasActionLine, { line, theme });
|
|
1337
|
+
}
|
|
1306
1338
|
if (line.kind === "runs") {
|
|
1307
1339
|
return React.createElement("div", { className: lineClassName },
|
|
1308
1340
|
React.createElement(Runs, { runs: line.runs, theme }));
|
|
@@ -3448,8 +3480,12 @@ function StructuredChatBlocks({ lines, theme, controller = null }) {
|
|
|
3448
3480
|
// for a host that cannot render the canvas). In the browser the canvas
|
|
3449
3481
|
// pane updating IS the signal, so the chat skips them outright — desktop
|
|
3450
3482
|
// and mobile share this renderer, so one filter covers both.
|
|
3483
|
+
//
|
|
3484
|
+
// canvasAction lines are NOT skipped: pressing a button in the drawn page
|
|
3485
|
+
// is a real thing the viewer did, and dropping it leaves an unexplained
|
|
3486
|
+
// gap before the agent's reply. Line renders it as one collapsed row.
|
|
3451
3487
|
const blocks = React.useMemo(
|
|
3452
|
-
() => parseStructuredChatBlocks((lines || []).filter((line) => !line?.canvasUpdate
|
|
3488
|
+
() => parseStructuredChatBlocks((lines || []).filter((line) => !line?.canvasUpdate)),
|
|
3453
3489
|
[lines],
|
|
3454
3490
|
);
|
|
3455
3491
|
return React.createElement(StructuredBlockList, { blocks, theme, controller });
|
|
@@ -6576,6 +6612,13 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
|
|
|
6576
6612
|
// making, and stacking them put a destructive Reset next to an innocuous
|
|
6577
6613
|
// deep link. Mirrors the Manage-session dialog's General/Access tabs.
|
|
6578
6614
|
const [tab, setTab] = React.useState("session");
|
|
6615
|
+
// The canvas KV write policy (interactive-canvas-apps Part D): who may
|
|
6616
|
+
// write the app's shared state. null = not loaded / not supported.
|
|
6617
|
+
const [kvPolicy, setKvPolicy] = React.useState(null);
|
|
6618
|
+
// Who the VIEWER is on this canvas. Only an owner or an admin may change
|
|
6619
|
+
// the policy, so the control is disabled for everyone else rather than
|
|
6620
|
+
// letting them pick a value and then showing a refusal.
|
|
6621
|
+
const [kvMe, setKvMe] = React.useState(null);
|
|
6579
6622
|
const transport = controller.transport;
|
|
6580
6623
|
const sessionLink = `${window.location.origin}/?session=${encodeURIComponent(sessionId)}&view=canvas&slot=${slot}&max=1`;
|
|
6581
6624
|
// The token variants all resolve the same hashed row: one token, N doors,
|
|
@@ -6588,9 +6631,24 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
|
|
|
6588
6631
|
transport.getCanvasShareLink?.(sessionId, slot)
|
|
6589
6632
|
.then((state) => setInfo(state || { exists: false }))
|
|
6590
6633
|
.catch((err) => { setInfo({ exists: false }); setError(err?.message || String(err)); });
|
|
6634
|
+
if (typeof transport.readCanvasKv === "function") {
|
|
6635
|
+
transport.readCanvasKv(sessionId, slot, { limit: 1 })
|
|
6636
|
+
.then((read) => { setKvPolicy(read?.policy || "owner"); setKvMe(read?.me || null); })
|
|
6637
|
+
.catch(() => { setKvPolicy(null); setKvMe(null); });
|
|
6638
|
+
}
|
|
6591
6639
|
}, [transport, sessionId, slot]);
|
|
6592
6640
|
React.useEffect(() => { refresh(); }, [refresh]);
|
|
6593
6641
|
|
|
6642
|
+
const canSetKvPolicy = kvMe?.relation === "owner" || kvMe?.relation === "admin";
|
|
6643
|
+
|
|
6644
|
+
const setPolicy = (value) => {
|
|
6645
|
+
setBusy(true); setError("");
|
|
6646
|
+
transport.setCanvasKvAccess(sessionId, slot, value)
|
|
6647
|
+
.then(() => setKvPolicy(value))
|
|
6648
|
+
.catch((err) => setError(err?.message || String(err)))
|
|
6649
|
+
.finally(() => setBusy(false));
|
|
6650
|
+
};
|
|
6651
|
+
|
|
6594
6652
|
const copy = (label, text) => {
|
|
6595
6653
|
navigator.clipboard?.writeText(text)
|
|
6596
6654
|
.then(() => { setCopied(label); window.setTimeout(() => setCopied(""), 1500); })
|
|
@@ -6648,7 +6706,34 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
|
|
|
6648
6706
|
role: "tabpanel", "aria-hidden": tab === "session" ? undefined : "true",
|
|
6649
6707
|
},
|
|
6650
6708
|
React.createElement("div", { className: "ps-canvas-share-sub" }, "Opens the portal signed in, canvas full screen. Session visibility rules apply."),
|
|
6651
|
-
linkRows(sessionLink, "session")
|
|
6709
|
+
linkRows(sessionLink, "session"),
|
|
6710
|
+
// Who may WRITE the app's shared state. Stated in words:
|
|
6711
|
+
// "readers" reaches everyone the session is read-shared
|
|
6712
|
+
// with, which a dialog that only says "link" hides.
|
|
6713
|
+
kvPolicy !== null
|
|
6714
|
+
? React.createElement("div", { className: "ps-canvas-share-policy" },
|
|
6715
|
+
React.createElement("label", { className: "ps-canvas-share-sub", htmlFor: "ps-canvas-kv-policy" }, "Who can edit inside this app"),
|
|
6716
|
+
React.createElement("select", {
|
|
6717
|
+
id: "ps-canvas-kv-policy",
|
|
6718
|
+
className: "ps-canvas-slot-select",
|
|
6719
|
+
value: kvPolicy,
|
|
6720
|
+
disabled: busy || !canSetKvPolicy,
|
|
6721
|
+
onChange: (event) => setPolicy(event.target.value),
|
|
6722
|
+
},
|
|
6723
|
+
React.createElement("option", { value: "owner" }, "Only people who can edit the session"),
|
|
6724
|
+
React.createElement("option", { value: "readers" }, "Anyone who can open the session"),
|
|
6725
|
+
React.createElement("option", { value: "link" }, "Anyone who can open the session, plus link viewers")),
|
|
6726
|
+
React.createElement("div", { className: "ps-canvas-share-sub" },
|
|
6727
|
+
kvPolicy === "owner"
|
|
6728
|
+
? "You, admins, and anyone you shared the session with as “write”. People with view-only access can watch the app but not change anything in it."
|
|
6729
|
+
: kvPolicy === "link"
|
|
6730
|
+
? "Your view-only people can edit inside the app. Anyone holding the share link can watch it — link holders can never edit."
|
|
6731
|
+
: "Adds your view-only people: they can edit inside the app. It does not let them message the agent."),
|
|
6732
|
+
canSetKvPolicy
|
|
6733
|
+
? null
|
|
6734
|
+
: React.createElement("div", { className: "ps-canvas-share-sub" },
|
|
6735
|
+
"Only the session owner or an admin can change this."))
|
|
6736
|
+
: null),
|
|
6652
6737
|
React.createElement("div", {
|
|
6653
6738
|
className: `ps-canvas-share-panel${tab === "public" ? " is-active" : ""}`,
|
|
6654
6739
|
role: "tabpanel", "aria-hidden": tab === "public" ? undefined : "true",
|
|
@@ -6753,6 +6838,9 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
6753
6838
|
const autoRetryRef = React.useRef({ rev: 0, used: false });
|
|
6754
6839
|
const hostRef = React.useRef(null);
|
|
6755
6840
|
const liveIframeElRef = React.useRef(null);
|
|
6841
|
+
// The staging frame's element, so the KV bridge can answer a page's
|
|
6842
|
+
// `ready` posted at script time (before promote). See the bridge below.
|
|
6843
|
+
const stagingIframeElRef = React.useRef(null);
|
|
6756
6844
|
const hostBox = useElementBox(hostRef);
|
|
6757
6845
|
// The frame geometry each revision LOADED at. A sandboxed page cannot be
|
|
6758
6846
|
// poked after the fact (no reaching into an opaque origin), and pages
|
|
@@ -6779,18 +6867,99 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
6779
6867
|
if (!controller || !sessionId) return undefined;
|
|
6780
6868
|
const onMessage = (event) => {
|
|
6781
6869
|
const frameEl = liveIframeElRef.current;
|
|
6782
|
-
if (!frameEl || !event.source || event.source !== frameEl.contentWindow) return;
|
|
6783
6870
|
const payload = event.data;
|
|
6784
|
-
if (!payload || typeof payload !== "object" ||
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6871
|
+
if (!payload || typeof payload !== "object" || !event.source) return;
|
|
6872
|
+
const fromLive = Boolean(frameEl && event.source === frameEl.contentWindow);
|
|
6873
|
+
// A document runs its scripts in the STAGING frame first and is
|
|
6874
|
+
// promoted on load — so a page that calls CanvasKV() at script
|
|
6875
|
+
// time posts `ready` before this frame is live. KV READS are
|
|
6876
|
+
// safe to answer from the staging frame of THIS canvas (it holds
|
|
6877
|
+
// the revision about to go live; the reply goes back to the
|
|
6878
|
+
// window that asked). Writes and actions stay live-frame only.
|
|
6879
|
+
const fromStaging = Boolean(stagingIframeElRef.current && event.source === stagingIframeElRef.current.contentWindow);
|
|
6880
|
+
if (!fromLive && !fromStaging) return;
|
|
6881
|
+
if (payload.type === "canvas-action") {
|
|
6882
|
+
if (!fromLive) return;
|
|
6883
|
+
controller.submitCanvasAction(sessionId, payload).then((result) => {
|
|
6884
|
+
if (result && result.ok === false && typeof console !== "undefined") {
|
|
6885
|
+
console.warn(`canvas action rejected: ${result.reason}`);
|
|
6886
|
+
}
|
|
6887
|
+
}).catch(() => {});
|
|
6888
|
+
return;
|
|
6889
|
+
}
|
|
6890
|
+
// The KV bridge (door 1). The page never names the session or the
|
|
6891
|
+
// slot — both come from THIS frame — and never sends `by`: the
|
|
6892
|
+
// server stamps it from the signed-in principal.
|
|
6893
|
+
if (payload.type === "canvas-kv") {
|
|
6894
|
+
const id = payload.id;
|
|
6895
|
+
const transport = controller.transport;
|
|
6896
|
+
// Reply to the window that ASKED (live or staging), never to
|
|
6897
|
+
// whichever frame is live by the time the request lands.
|
|
6898
|
+
const source = event.source;
|
|
6899
|
+
const post = (message) => {
|
|
6900
|
+
try { source.postMessage(message, "*"); } catch { /* frame mid-teardown */ }
|
|
6901
|
+
};
|
|
6902
|
+
const fail = (error, code = "ERROR") => post({ type: "canvas-kv-result", id, ok: false, error: String(error?.message || error || "failed"), code: error?.code || code });
|
|
6903
|
+
if (typeof transport?.readCanvasKv !== "function") { fail("the canvas KV store is not available on this deployment", "UNAVAILABLE"); return; }
|
|
6904
|
+
const op = String(payload.op || "");
|
|
6905
|
+
if (!fromLive && (op === "put" || op === "delete")) {
|
|
6906
|
+
fail("the canvas is still loading; retry the write in a moment", "NOT_READY");
|
|
6907
|
+
return;
|
|
6788
6908
|
}
|
|
6789
|
-
|
|
6909
|
+
if (op === "ready" || op === "list") {
|
|
6910
|
+
transport.readCanvasKv(sessionId, slot, { prefix: payload.prefix ?? null, limit: payload.limit ?? null, after: payload.after ?? null })
|
|
6911
|
+
.then((read) => post({
|
|
6912
|
+
type: op === "ready" ? "canvas-kv-ready" : "canvas-kv-result",
|
|
6913
|
+
id, ok: true,
|
|
6914
|
+
entries: read?.entries ?? [], nextAfter: read?.nextAfter ?? null,
|
|
6915
|
+
me: read?.me ?? null, canWrite: Boolean(read?.me?.canWrite), policy: read?.policy ?? "owner",
|
|
6916
|
+
}))
|
|
6917
|
+
.catch(fail);
|
|
6918
|
+
return;
|
|
6919
|
+
}
|
|
6920
|
+
if (op === "get") {
|
|
6921
|
+
transport.readCanvasKv(sessionId, slot, { key: payload.key })
|
|
6922
|
+
.then((read) => post({ type: "canvas-kv-result", id, ok: true, entry: read?.entries?.[0] ?? null }))
|
|
6923
|
+
.catch(fail);
|
|
6924
|
+
return;
|
|
6925
|
+
}
|
|
6926
|
+
if (op === "put" || op === "delete") {
|
|
6927
|
+
const one = op === "put"
|
|
6928
|
+
? { op: "put", key: payload.key, value: payload.value, ...(payload.ifMatch != null ? { ifMatch: payload.ifMatch } : {}) }
|
|
6929
|
+
: { op: "delete", key: payload.key, ...(payload.ifMatch != null ? { ifMatch: payload.ifMatch } : {}) };
|
|
6930
|
+
transport.writeCanvasKv(sessionId, slot, [one])
|
|
6931
|
+
.then((written) => {
|
|
6932
|
+
const r = written?.results?.[0] ?? { ok: false, error: "no result" };
|
|
6933
|
+
post({ type: "canvas-kv-result", id, ok: Boolean(r.ok), key: r.key, rev: r.rev, capped: r.capped, error: r.error, code: r.code });
|
|
6934
|
+
})
|
|
6935
|
+
.catch(fail);
|
|
6936
|
+
return;
|
|
6937
|
+
}
|
|
6938
|
+
fail(`unknown canvas-kv op ${op}`, "INVALID_REQUEST");
|
|
6939
|
+
}
|
|
6790
6940
|
};
|
|
6791
6941
|
window.addEventListener("message", onMessage);
|
|
6792
6942
|
return () => window.removeEventListener("message", onMessage);
|
|
6793
|
-
}, [controller, sessionId]);
|
|
6943
|
+
}, [controller, sessionId, slot]);
|
|
6944
|
+
|
|
6945
|
+
// Live KV changes for this slot → the page, as `canvas-kv-change`. A
|
|
6946
|
+
// pointer-only ping (value too big for the notify) fetches that one key.
|
|
6947
|
+
React.useEffect(() => {
|
|
6948
|
+
if (!controller || !sessionId || typeof controller.subscribeCanvasKv !== "function") return undefined;
|
|
6949
|
+
return controller.subscribeCanvasKv(sessionId, (change) => {
|
|
6950
|
+
if (!change || Number(change.slot) !== Number(slot)) return;
|
|
6951
|
+
const frameWindow = liveIframeElRef.current?.contentWindow;
|
|
6952
|
+
if (!frameWindow) return;
|
|
6953
|
+
const deliver = (entry) => {
|
|
6954
|
+
try { frameWindow.postMessage({ type: "canvas-kv-change", key: change.key, rev: change.rev, op: change.op, ...(entry ? { v: entry.v, by: entry.by, at: entry.at } : {}) }, "*"); } catch { /* frame mid-teardown */ }
|
|
6955
|
+
};
|
|
6956
|
+
if (change.op === "delete") { deliver(null); return; }
|
|
6957
|
+
if (change.value && typeof change.value === "object") { deliver({ v: change.value.v, by: change.value.by, at: change.value.at }); return; }
|
|
6958
|
+
controller.transport?.readCanvasKv?.(sessionId, slot, { key: change.key })
|
|
6959
|
+
.then((read) => deliver(read?.entries?.[0] ?? null))
|
|
6960
|
+
.catch(() => { /* the page's next list catches up */ });
|
|
6961
|
+
});
|
|
6962
|
+
}, [controller, sessionId, slot]);
|
|
6794
6963
|
|
|
6795
6964
|
React.useEffect(() => {
|
|
6796
6965
|
if (!controller || !sessionId || !latestRev) return undefined;
|
|
@@ -6971,7 +7140,9 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
6971
7140
|
return React.createElement("div", { ref: hostRef, className: "ps-html-preview-host is-fitting" },
|
|
6972
7141
|
[live, staging].filter(Boolean).map((frame) => React.createElement("iframe", {
|
|
6973
7142
|
key: `canvas:${frame.rev}:${frame.url}`,
|
|
6974
|
-
ref: frame === live
|
|
7143
|
+
ref: frame === live
|
|
7144
|
+
? ((el) => { liveIframeElRef.current = el; })
|
|
7145
|
+
: ((el) => { stagingIframeElRef.current = el; }),
|
|
6975
7146
|
className: `ps-html-preview-frame${frame === live ? "" : " is-staging"}`,
|
|
6976
7147
|
style: zoomStyle,
|
|
6977
7148
|
src: frame.url,
|
|
@@ -9938,14 +10109,42 @@ function ProviderSystemSpend({ view }) {
|
|
|
9938
10109
|
? React.createElement("div", { className: "ps-budget-empty is-error", role: "alert" }, spend.error)
|
|
9939
10110
|
: null,
|
|
9940
10111
|
spend.loaded ? React.createElement(React.Fragment, null,
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
10112
|
+
// The headline: total tokens and turns as two stat tiles, then a
|
|
10113
|
+
// per-model table with a share bar so the mix reads at a glance
|
|
10114
|
+
// (77.9M of 94.7M on one model is the fact that matters).
|
|
10115
|
+
React.createElement("div", { className: "ps-budget-system-stats" },
|
|
10116
|
+
React.createElement("div", { className: "ps-budget-system-stat" },
|
|
10117
|
+
React.createElement("strong", null, spend.tokensLabel),
|
|
10118
|
+
React.createElement("span", null, "tokens")),
|
|
10119
|
+
React.createElement("div", { className: "ps-budget-system-stat" },
|
|
10120
|
+
React.createElement("strong", null, formatCompactNumber(spend.turns)),
|
|
10121
|
+
React.createElement("span", null, spend.turns === 1 ? "turn" : "turns")),
|
|
10122
|
+
spend.turns > 0
|
|
10123
|
+
? React.createElement("div", { className: "ps-budget-system-stat" },
|
|
10124
|
+
React.createElement("strong", null, formatCompactNumber(Math.round(spend.tokens / spend.turns))),
|
|
10125
|
+
React.createElement("span", null, "tokens / turn"))
|
|
10126
|
+
: null),
|
|
9944
10127
|
spend.models.length
|
|
9945
|
-
? React.createElement("
|
|
9946
|
-
|
|
9947
|
-
React.createElement("
|
|
9948
|
-
|
|
10128
|
+
? React.createElement("table", { className: "ps-budget-system-models" },
|
|
10129
|
+
React.createElement("thead", null,
|
|
10130
|
+
React.createElement("tr", null,
|
|
10131
|
+
React.createElement("th", { scope: "col" }, "Model"),
|
|
10132
|
+
React.createElement("th", { scope: "col", className: "is-num" }, "Tokens"),
|
|
10133
|
+
React.createElement("th", { scope: "col", className: "is-share" }, "Share"),
|
|
10134
|
+
React.createElement("th", { scope: "col", className: "is-num" }, "Turns"))),
|
|
10135
|
+
React.createElement("tbody", null,
|
|
10136
|
+
spend.models.map((model) => {
|
|
10137
|
+
const share = spend.tokens > 0 ? model.tokens / spend.tokens : 0;
|
|
10138
|
+
const pct = Math.round(share * 100);
|
|
10139
|
+
return React.createElement("tr", { key: model.key },
|
|
10140
|
+
React.createElement("td", { className: "ps-budget-system-model-name" }, modelLabel(model.label)),
|
|
10141
|
+
React.createElement("td", { className: "is-num" }, model.tokensLabel),
|
|
10142
|
+
React.createElement("td", { className: "is-share" },
|
|
10143
|
+
React.createElement("span", { className: "ps-budget-system-bar", role: "img", "aria-label": `${pct}% of system spend` },
|
|
10144
|
+
React.createElement("span", { className: "ps-budget-system-bar-fill", style: { width: `${Math.max(share > 0 ? 1 : 0, share * 100)}%` } })),
|
|
10145
|
+
React.createElement("span", { className: "ps-budget-system-pct" }, `${pct}%`)),
|
|
10146
|
+
React.createElement("td", { className: "is-num" }, formatCompactNumber(model.turns)));
|
|
10147
|
+
})))
|
|
9949
10148
|
: React.createElement("div", { className: "ps-budget-sub" }, "No system spend in this range.")) : null);
|
|
9950
10149
|
}
|
|
9951
10150
|
|
|
@@ -12003,13 +12202,18 @@ function AdminPackageDetailPane({ controller, view }) {
|
|
|
12003
12202
|
React.createElement("span", null, version.dateText),
|
|
12004
12203
|
version.active
|
|
12005
12204
|
? React.createElement("span", { className: "is-ok" }, "● active")
|
|
12006
|
-
: (detail.
|
|
12205
|
+
: (detail.canEdit
|
|
12007
12206
|
? React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act("pin", version.semver), disabled: Boolean(pending) }, "Pin")
|
|
12008
12207
|
: null)))))
|
|
12009
12208
|
: null,
|
|
12010
|
-
|
|
12209
|
+
// Editors live on the shared copy only. Everyone sees the list;
|
|
12210
|
+
// only the owner/admin changes it.
|
|
12211
|
+
detail.scope === "shared"
|
|
12212
|
+
? React.createElement(AdminPackageEditorsSection, { controller, detail, pending })
|
|
12213
|
+
: null,
|
|
12214
|
+
detail.canEdit
|
|
12011
12215
|
? React.createElement("div", { className: "ps-admin-detail__actions" },
|
|
12012
|
-
detail.source
|
|
12216
|
+
detail.source && detail.canManage
|
|
12013
12217
|
? React.createElement("button", { type: "button", className: "ps-primary-button", onClick: act("sync"), disabled: Boolean(pending) },
|
|
12014
12218
|
pending === "sync" ? "Syncing…" : "Sync now")
|
|
12015
12219
|
: null,
|
|
@@ -12034,8 +12238,11 @@ function AdminPackageDetailPane({ controller, view }) {
|
|
|
12034
12238
|
disabled: Boolean(pending) || !detail.activeSemver,
|
|
12035
12239
|
title: `Publish ${detail.name}@${detail.activeSemver ?? "?"} into the existing shared package`,
|
|
12036
12240
|
}, pending === "republish" ? "Publishing…" : `Publish ${detail.activeSemver ?? ""} to shared`)
|
|
12037
|
-
:
|
|
12038
|
-
detail.scope === "shared" ? "
|
|
12241
|
+
: (detail.canManage
|
|
12242
|
+
? React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act(detail.scope === "shared" ? "demote" : "promote"), disabled: Boolean(pending),
|
|
12243
|
+
title: detail.scope === "shared" && detail.editors.length ? "Demoting revokes every editor grant" : undefined },
|
|
12244
|
+
detail.scope === "shared" ? "Demote to user" : "Promote to shared")
|
|
12245
|
+
: null),
|
|
12039
12246
|
detail.scope === "shared" && !detail.hasOwnUserCounterpart
|
|
12040
12247
|
? React.createElement("button", {
|
|
12041
12248
|
type: "button",
|
|
@@ -12047,14 +12254,110 @@ function AdminPackageDetailPane({ controller, view }) {
|
|
|
12047
12254
|
: null,
|
|
12048
12255
|
React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act(detail.enabled ? "disable" : "enable"), disabled: Boolean(pending) },
|
|
12049
12256
|
detail.enabled ? "Disable" : "Enable"),
|
|
12050
|
-
|
|
12257
|
+
detail.canManage
|
|
12258
|
+
? React.createElement("button", { type: "button", className: "ps-mini-button is-danger", onClick: confirmDelete, disabled: Boolean(pending) }, "Delete")
|
|
12259
|
+
: null)
|
|
12051
12260
|
: React.createElement("p", { className: "ps-admin-console__hint" },
|
|
12052
|
-
"Read-only: only the package creator or an admin can modify it."),
|
|
12261
|
+
"Read-only: only the package creator, a granted editor, or an admin can modify it."),
|
|
12053
12262
|
detail.actionError
|
|
12054
12263
|
? React.createElement("div", { className: "ps-admin-console__error", role: "alert" }, detail.actionError)
|
|
12055
12264
|
: null);
|
|
12056
12265
|
}
|
|
12057
12266
|
|
|
12267
|
+
/**
|
|
12268
|
+
* Editors of a SHARED package: named users who may publish, pin and
|
|
12269
|
+
* enable/disable it (never scope, delete, or this list). Everyone who can
|
|
12270
|
+
* see the package sees the list; only the owner/admin changes it. Grantees
|
|
12271
|
+
* are picked from the same member directory the session-share dialog uses.
|
|
12272
|
+
*/
|
|
12273
|
+
function AdminPackageEditorsSection({ controller, detail, pending }) {
|
|
12274
|
+
const [query, setQuery] = React.useState("");
|
|
12275
|
+
const [directory, setDirectory] = React.useState([]);
|
|
12276
|
+
React.useEffect(() => {
|
|
12277
|
+
if (!detail.canManage || typeof controller.transport.listKnownUsers !== "function") return undefined;
|
|
12278
|
+
let cancelled = false;
|
|
12279
|
+
controller.transport.listKnownUsers({ limit: 500 })
|
|
12280
|
+
.then((users) => { if (!cancelled) setDirectory(Array.isArray(users) ? users : []); })
|
|
12281
|
+
.catch(() => {});
|
|
12282
|
+
return () => { cancelled = true; };
|
|
12283
|
+
}, [controller, detail.canManage]);
|
|
12284
|
+
|
|
12285
|
+
const editorKeys = new Set(detail.editors.map((e) => `${e.provider}${e.subject}`));
|
|
12286
|
+
const q = query.trim().toLowerCase();
|
|
12287
|
+
const suggestions = q
|
|
12288
|
+
? directory.filter((u) => !editorKeys.has(`${u.provider}${u.subject}`) && (
|
|
12289
|
+
(u.displayName && u.displayName.toLowerCase().includes(q))
|
|
12290
|
+
|| (u.email && u.email.toLowerCase().includes(q))
|
|
12291
|
+
|| (u.subject && u.subject.toLowerCase().includes(q)))).slice(0, 8)
|
|
12292
|
+
: [];
|
|
12293
|
+
const grant = (user) => {
|
|
12294
|
+
if (!user?.provider || !user?.subject) return;
|
|
12295
|
+
setQuery("");
|
|
12296
|
+
controller.runAdminPackageAction("grantEditor", { provider: user.provider, subject: user.subject });
|
|
12297
|
+
};
|
|
12298
|
+
// Enter/Add grants only an EXACT match, or the single remaining
|
|
12299
|
+
// suggestion. A typo must never hand write access to whoever happens
|
|
12300
|
+
// to sort first in the directory — the person picks from the list.
|
|
12301
|
+
const grantTyped = () => {
|
|
12302
|
+
const text = query.trim();
|
|
12303
|
+
if (!text) return;
|
|
12304
|
+
const exact = directory.find((u) =>
|
|
12305
|
+
(u.displayName && u.displayName.toLowerCase() === text.toLowerCase())
|
|
12306
|
+
|| (u.email && u.email.toLowerCase() === text.toLowerCase())
|
|
12307
|
+
|| (u.subject && u.subject.toLowerCase() === text.toLowerCase()));
|
|
12308
|
+
if (exact) return grant(exact);
|
|
12309
|
+
if (suggestions.length === 1) return grant(suggestions[0]);
|
|
12310
|
+
};
|
|
12311
|
+
|
|
12312
|
+
return React.createElement("div", { className: "ps-admin-editors" },
|
|
12313
|
+
React.createElement("div", { className: "ps-admin-detail__label" }, "Editors"),
|
|
12314
|
+
detail.editors.length
|
|
12315
|
+
? React.createElement("div", { className: "ps-admin-detail__chips" },
|
|
12316
|
+
detail.editors.map((e) => React.createElement("span", {
|
|
12317
|
+
key: `${e.provider}${e.subject}`,
|
|
12318
|
+
className: "ps-agent-chip",
|
|
12319
|
+
title: `${e.provider}:${e.subject}${e.grantedByDisplay ? ` · granted by ${e.grantedByDisplay}` : ""}`,
|
|
12320
|
+
},
|
|
12321
|
+
e.label,
|
|
12322
|
+
detail.canManage
|
|
12323
|
+
? React.createElement("button", {
|
|
12324
|
+
type: "button",
|
|
12325
|
+
className: "ps-chip-remove",
|
|
12326
|
+
"aria-label": `Remove editor ${e.label}`,
|
|
12327
|
+
disabled: Boolean(pending),
|
|
12328
|
+
onClick: () => controller.runAdminPackageAction("revokeEditor", { provider: e.provider, subject: e.subject }),
|
|
12329
|
+
}, "×")
|
|
12330
|
+
: null)))
|
|
12331
|
+
: React.createElement("p", { className: "ps-admin-console__hint" },
|
|
12332
|
+
detail.canManage
|
|
12333
|
+
? "No editors yet. Add a person to let them publish, pin and enable this package. Grants are revoked if the package is demoted."
|
|
12334
|
+
: "Only the owner and admins can change this package."),
|
|
12335
|
+
detail.canManage
|
|
12336
|
+
? React.createElement("div", { className: "ps-admin-editors__add" },
|
|
12337
|
+
React.createElement("input", {
|
|
12338
|
+
type: "text",
|
|
12339
|
+
className: "ps-input",
|
|
12340
|
+
placeholder: "Add editor by name, email, or id…",
|
|
12341
|
+
value: query,
|
|
12342
|
+
disabled: Boolean(pending),
|
|
12343
|
+
onChange: (event) => setQuery(event.target.value),
|
|
12344
|
+
onKeyDown: (event) => { if (event.key === "Enter") { event.preventDefault(); grantTyped(); } },
|
|
12345
|
+
"aria-label": "Add editor",
|
|
12346
|
+
}),
|
|
12347
|
+
React.createElement("button", { type: "button", className: "ps-mini-button", onClick: grantTyped, disabled: Boolean(pending) || !query.trim() }, "Add"),
|
|
12348
|
+
suggestions.length
|
|
12349
|
+
? React.createElement("div", { className: "ps-admin-editors__suggestions", role: "listbox" },
|
|
12350
|
+
suggestions.map((u) => React.createElement("button", {
|
|
12351
|
+
key: `${u.provider}${u.subject}`,
|
|
12352
|
+
type: "button",
|
|
12353
|
+
role: "option",
|
|
12354
|
+
className: "ps-admin-editors__suggestion",
|
|
12355
|
+
onClick: () => grant(u),
|
|
12356
|
+
}, u.displayName || u.email || u.subject, u.email && u.displayName ? React.createElement("i", null, ` ${u.email}`) : null)))
|
|
12357
|
+
: null)
|
|
12358
|
+
: null);
|
|
12359
|
+
}
|
|
12360
|
+
|
|
12058
12361
|
function AdminPackageWorkspacePane({ controller, view, layout = null, onLayout = null }) {
|
|
12059
12362
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
12060
12363
|
const theme = getTheme(themeId);
|
package/web/api/canvas-plane.js
CHANGED
|
@@ -54,12 +54,24 @@ export function createCanvasPlane({
|
|
|
54
54
|
if (payload?.schema && payload.schema !== schema) return;
|
|
55
55
|
const handlers = subscribers.get(payload?.sessionId);
|
|
56
56
|
if (!handlers) return;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
// The KV branch is a separate dispatch path from doc/data: one
|
|
58
|
+
// `rev` per KEY (max-wins), never the slot's seq chain. Feeding
|
|
59
|
+
// per-key revs into seq would read as a gap and resync-storm.
|
|
60
|
+
const update = payload.kind === "kv"
|
|
61
|
+
? {
|
|
62
|
+
slot: Number(payload.slot),
|
|
63
|
+
kind: "kv",
|
|
64
|
+
key: String(payload.key ?? ""),
|
|
65
|
+
rev: Number(payload.rev),
|
|
66
|
+
op: payload.op === "delete" ? "delete" : "put",
|
|
67
|
+
...(payload.value !== undefined ? { value: payload.value } : {}),
|
|
68
|
+
}
|
|
69
|
+
: {
|
|
70
|
+
slot: Number(payload.slot),
|
|
71
|
+
seq: Number(payload.seq),
|
|
72
|
+
kind: payload.kind === "doc" ? "doc" : "data",
|
|
73
|
+
...(payload.patch && typeof payload.patch === "object" ? { patch: payload.patch } : {}),
|
|
74
|
+
};
|
|
63
75
|
for (const cb of handlers) {
|
|
64
76
|
try { cb(update); } catch { /* one bad socket must not stop the fan-out */ }
|
|
65
77
|
}
|
package/web/api/ws.js
CHANGED
|
@@ -33,6 +33,15 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
33
33
|
let shareScope = null;
|
|
34
34
|
let shareRevalidateTimer = null;
|
|
35
35
|
let auth = null;
|
|
36
|
+
// Messages that arrive while auth is still being resolved (a client
|
|
37
|
+
// that sends `subscribeCanvas` on `open`) must not be lost: the real
|
|
38
|
+
// handler is attached only after the awaits below. Buffer them and
|
|
39
|
+
// replay once it is. Measured before this: the share view lost its
|
|
40
|
+
// subscribe about half the time.
|
|
41
|
+
const early = [];
|
|
42
|
+
let buffering = true;
|
|
43
|
+
const earlyListener = (raw) => { if (buffering) early.push(raw); };
|
|
44
|
+
ws.on("message", earlyListener);
|
|
36
45
|
if (shareToken) {
|
|
37
46
|
if (typeof runtime.resolveCanvasShareToken === "function") {
|
|
38
47
|
shareScope = await runtime.resolveCanvasShareToken(shareToken).catch(() => null);
|
|
@@ -84,6 +93,15 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
84
93
|
|
|
85
94
|
send({ type: "ready" });
|
|
86
95
|
|
|
96
|
+
// The real handler is registered synchronously right below. Stop
|
|
97
|
+
// buffering NOW (so nothing is delivered twice) and replay what was
|
|
98
|
+
// buffered after this tick, when the handler exists.
|
|
99
|
+
buffering = false;
|
|
100
|
+
setImmediate(() => {
|
|
101
|
+
ws.off("message", earlyListener);
|
|
102
|
+
for (const raw of early.splice(0)) ws.emit("message", raw, false);
|
|
103
|
+
});
|
|
104
|
+
|
|
87
105
|
ws.on("message", async (raw) => {
|
|
88
106
|
let message;
|
|
89
107
|
try {
|
|
@@ -109,12 +127,14 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
109
127
|
send({ type: "error", scope: "canvas", sessionId, error: "canvas plane unavailable" });
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
130
|
+
// A bearer is never told the session id: the token IS the
|
|
131
|
+
// address, and the share view does not need it.
|
|
112
132
|
const unsubscribe = plane.subscribe(sessionId, (update) => {
|
|
113
133
|
if (Number(update?.slot) !== shareScope.slot) return;
|
|
114
|
-
send({ type: "canvasLive",
|
|
134
|
+
send({ type: "canvasLive", ...update });
|
|
115
135
|
});
|
|
116
136
|
canvasSubscriptions.set(sessionId, unsubscribe);
|
|
117
|
-
send({ type: "subscribedCanvas"
|
|
137
|
+
send({ type: "subscribedCanvas" });
|
|
118
138
|
} else if (type === "unsubscribeCanvas") {
|
|
119
139
|
const unsubscribe = canvasSubscriptions.get(shareScope.sessionId);
|
|
120
140
|
if (unsubscribe) {
|