pilotswarm 0.5.37 → 0.5.38
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/package.json +3 -3
- package/ui/core/src/controller.js +97 -20
- package/ui/core/src/reducer.js +213 -15
- package/ui/core/src/selectors.js +83 -3
- package/ui/core/src/state.js +90 -2
- package/ui/core/src/themes/index.js +1 -1
- package/ui/react/src/web-app.js +1038 -218
- package/web/auth/config.js +3 -0
- package/web/auth/index.js +8 -1
- package/web/auth/normalize/proxy.js +48 -0
- package/web/auth/providers/proxy.js +141 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DlvPH-8O.js → abnfDiagram-VRR7QNED-DrJhXhSQ.js} +1 -1
- package/web/dist/assets/{arc-r1liz5e5.js → arc-BEaG9DWd.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bci5dNH3.js → architectureDiagram-ZJ3FMSHR-D9fc1Imx.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Bug32AXp.js → blockDiagram-677ZJIJ3-rS5Eikvt.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DwxSFqgE.js → c4Diagram-LMCZKHZV-HKLlX5cR.js} +1 -1
- package/web/dist/assets/channel-DRy7FvyF.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-B_dbI_xg.js → chunk-2Q5K7J3B-kq0TyLSB.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-BkF68W-R.js → chunk-32BRIVSS-O-LMarT5.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BSbNIbJu.js → chunk-5VM5RSS4-Di4h0CAu.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-Blv_Iqa2.js → chunk-EX3LRPZG-C8rgqUOt.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Dk_dBFKd.js → chunk-JWPE2WC7-BSH41q4T.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-DqNFBrfw.js → chunk-MOJQB5TN-BAFuQHAC.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-Dlv7Svpb.js → chunk-RYQCIY6F-DAg6JOs7.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DozGikwu.js → chunk-V7JOEXUC-DbVrT8BG.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-CmkqvGT4.js → chunk-VR4S4FIN-C6f5_Grp.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DyByai5N.js → chunk-XXDRQBXY-CqcCYpqA.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C35RhB8X.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C35RhB8X.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bv-gRVvA.js → cose-bilkent-JH36ORCC-DJw04eFh.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DRnAsyZU.js → cynefin-VYW2F7L2-CpjwdEIU.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-B1fnUtjo.js → cynefinDiagram-TSTJHNR4-RqEIWoT0.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DM6-QENB.js → dagre-VKFMJZFB-C1eGQNK2.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BPxxnLdU.js → diagram-FQU43EPY-BODLQ7dz.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-BgtJT3qF.js → diagram-G47NLZAW-DlhmukMC.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-uekeIzsy.js → diagram-NH7WQ7WH-Ba_jZoCN.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BVy3JZWE.js → diagram-OA4YK3LP-DnSiAJBG.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-BWdjUr32.js → diagram-WEI45ONY-B3KoQ7_W.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DD4fwhNK.js → ebnfDiagram-CCIWWBDH-ChnFc5gf.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-BllD_6Kh.js → erDiagram-Q63AITRT-D2KkJXLZ.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-QEYKkAUN.js → flowDiagram-23GEKE2U-CmybZufj.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-uZcfKTSW.js → ganttDiagram-NO4QXBWP-BzPurW_v.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-XPdbPaH-.js → gitGraphDiagram-IHSO6WYX-DAbXl4gO.js} +1 -1
- package/web/dist/assets/{index-D1itTxvB.js → index-CRY94nFp.js} +6 -6
- package/web/dist/assets/index-DtL0lbis.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DXCnQOJs.js → infoDiagram-FWYZ7A6U-C6qGjNA1.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-687_4FBy.js → ishikawaDiagram-FXEZZL3T-D2d1tO-9.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-GXlKuda8.js → journeyDiagram-5HDEW3XC-fP13w1b7.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-C38-2DvN.js → kanban-definition-HUTT4EX6-BJfWAXZ0.js} +1 -1
- package/web/dist/assets/{linear-lGPXiBcz.js → linear-CltApkHf.js} +1 -1
- package/web/dist/assets/{mermaid.core-C_neEtab.js → mermaid.core-B1Dwetk3.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BaJ9tpEG.js → mindmap-definition-LN4V7U3C-CdRUD7LF.js} +1 -1
- package/web/dist/assets/{papaparse.min-rDwKYbBf.js → papaparse.min-C1y3wBH1.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BdXJLYgY.js → pegDiagram-2B236MQR-CeQkqN6Q.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-DU19t6fM.js → pieDiagram-ENE6RG2P-Dqgc87jA.js} +1 -1
- package/web/dist/assets/pilotswarm-DayYdi39.js +96 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CBkOdPp0.js → quadrantDiagram-ABIIQ3AL-CD7f-w6m.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-D2DMcGDw.js → railroadDiagram-RFXS5EU6-CcsAijSC.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-Bx9tF1pr.js → requirementDiagram-TGXJPOKE-BkzF80pL.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DcLlRfsi.js → sankeyDiagram-HTMAVEWB-D9XOFw7Y.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-xr6UVRBO.js → sequenceDiagram-DBY2YBRQ-BFfaS_Sz.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-0fUkSXmg.js → sizeCapture-X5ZJPWSS-CDJV16tP.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BWgJGGpg.js → stateDiagram-2N3HPSRC-C0E-MPXO.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BMqXeMJu.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-CZKya4_i.js → swimlanes-5IMT3BWC-n5Vsv7n9.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CGl4gRnH.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-Df8BpeAu.js → timeline-definition-FHXFAJF6-DW2gV8-n.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-ChX5bjT6.js → vennDiagram-L72KCM5P-D9dbDm08.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-Dr48BgaJ.js → wardleyDiagram-EHGQE667-CIcG7fiC.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Cia_MFfc.js → xychartDiagram-FW5EYKEG-elEIFsc9.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/channel-jVpSYat3.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C78zSajE.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C78zSajE.js +0 -1
- package/web/dist/assets/index-BuDSoRIL.css +0 -1
- package/web/dist/assets/pilotswarm-BNKGG936.js +0 -96
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DmAC8DdB.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CVYx20_X.js +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.38",
|
|
4
4
|
"description": "PilotSwarm application package: terminal UI, browser portal + Web API server, and MCP server — one install, three bins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -81,13 +81,13 @@
|
|
|
81
81
|
"ink": "^6.8.0",
|
|
82
82
|
"jose": "^6.2.2",
|
|
83
83
|
"mermaid": "^11.16.0",
|
|
84
|
-
"pilotswarm-sdk": "0.5.
|
|
84
|
+
"pilotswarm-sdk": "0.5.38",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@vitejs/plugin-react": "^5.1.0",
|
|
91
|
-
"vite": "
|
|
91
|
+
"vite": "7.3.5"
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UI_COMMANDS, FOCUS_REGIONS, INSPECTOR_TABS, cycleValue } from "./commands.js";
|
|
2
|
+
import { canvasKey as canvasPrefKey } from "./state.js";
|
|
2
3
|
import { parseAgentSourceLink } from "./repo-links.js";
|
|
3
4
|
import { importPackageFilesFromLink, readImportedPackageName } from "./repo-import.js";
|
|
4
5
|
import {
|
|
@@ -2414,6 +2415,17 @@ export class PilotSwarmUiController {
|
|
|
2414
2415
|
if (pendingGroupRows) {
|
|
2415
2416
|
this.dispatch({ type: "sessions/groupsLoaded", groups: pendingGroupRows, sessions });
|
|
2416
2417
|
}
|
|
2418
|
+
// Canvas summaries ride the list rows (session_canvases join) — seed
|
|
2419
|
+
// the store so rows mark on cold load, before any session is selected
|
|
2420
|
+
// or any live event arrives. Never regresses what live events already
|
|
2421
|
+
// know; see the canvas/seed reducer.
|
|
2422
|
+
const canvasSeed = [];
|
|
2423
|
+
for (const row of sessions) {
|
|
2424
|
+
for (const c of Array.isArray(row?.canvases) ? row.canvases : []) {
|
|
2425
|
+
canvasSeed.push({ sessionId: row.sessionId, slot: c.slot, name: c.name, latestRev: c.latestRev, sizeBytes: c.sizeBytes });
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (canvasSeed.length) this.dispatch({ type: "canvas/seed", entries: canvasSeed });
|
|
2417
2429
|
this.dispatch({ type: "sessions/loaded", sessions });
|
|
2418
2430
|
this.refreshOpenSessionOwnerFilterModal();
|
|
2419
2431
|
const selected = this.getState().sessions.activeSessionId;
|
|
@@ -4254,9 +4266,13 @@ export class PilotSwarmUiController {
|
|
|
4254
4266
|
applyCanvasUpdate(sessionId, event) {
|
|
4255
4267
|
const rev = Number(event?.data?.rev);
|
|
4256
4268
|
if (!sessionId || !Number.isFinite(rev) || rev <= 0) return false;
|
|
4269
|
+
const rawSlot = Number(event?.data?.slot);
|
|
4270
|
+
const slot = Number.isInteger(rawSlot) && rawSlot >= 1 && rawSlot <= 5 ? rawSlot : 1;
|
|
4257
4271
|
this.dispatch({
|
|
4258
4272
|
type: "canvas/updated",
|
|
4259
4273
|
sessionId,
|
|
4274
|
+
slot,
|
|
4275
|
+
...(typeof event?.data?.name === "string" ? { name: event.data.name } : {}),
|
|
4260
4276
|
rev,
|
|
4261
4277
|
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
4262
4278
|
sizeBytes: event?.data?.sizeBytes,
|
|
@@ -4265,7 +4281,9 @@ export class PilotSwarmUiController {
|
|
|
4265
4281
|
|
|
4266
4282
|
const state = this.getState();
|
|
4267
4283
|
if (sessionId !== state.sessions.activeSessionId) return true;
|
|
4268
|
-
|
|
4284
|
+
// The opt-out is PER SLOT: leaving one canvas by hand must not
|
|
4285
|
+
// silence a different canvas's first draw.
|
|
4286
|
+
if (state.canvas.prefs[canvasPrefKey(sessionId, slot)]?.optedOut) return true;
|
|
4269
4287
|
const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
|
|
4270
4288
|
if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
|
|
4271
4289
|
return true;
|
|
@@ -4274,7 +4292,7 @@ export class PilotSwarmUiController {
|
|
|
4274
4292
|
// when the mode is already "canvas", because a phone can be off its
|
|
4275
4293
|
// canvas tab while the mode state still says canvas — the tick is
|
|
4276
4294
|
// what brings the tab back. On desktop the repeat dispatch is a no-op.
|
|
4277
|
-
this.dispatch({ type: "canvas/flip", sessionId });
|
|
4295
|
+
this.dispatch({ type: "canvas/flip", sessionId, slot });
|
|
4278
4296
|
return true;
|
|
4279
4297
|
}
|
|
4280
4298
|
|
|
@@ -4337,29 +4355,55 @@ export class PilotSwarmUiController {
|
|
|
4337
4355
|
return;
|
|
4338
4356
|
}
|
|
4339
4357
|
try {
|
|
4358
|
+
// A 30-event window, not 1: five interleaved slots mean the top
|
|
4359
|
+
// event only describes the most recently drawn slot. Latest per
|
|
4360
|
+
// slot wins; every drawn slot gets its snapshot.
|
|
4340
4361
|
const rows = await this.transport.getSessionEventsBefore(
|
|
4341
|
-
sessionId, Number.MAX_SAFE_INTEGER,
|
|
4362
|
+
sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_updated"],
|
|
4342
4363
|
);
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
rev
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4364
|
+
const bySlot = new Map();
|
|
4365
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
4366
|
+
const slot = Number(row?.data?.slot) || 1;
|
|
4367
|
+
const rev = Number(row?.data?.rev) || 0;
|
|
4368
|
+
if (!bySlot.has(slot) || rev > (Number(bySlot.get(slot)?.data?.rev) || 0)) bySlot.set(slot, row);
|
|
4369
|
+
}
|
|
4370
|
+
// Always at least a slot-1 snapshot (rev 0 marks snapshotLoaded
|
|
4371
|
+
// so this does not respin).
|
|
4372
|
+
if (!bySlot.has(1)) bySlot.set(1, null);
|
|
4373
|
+
let anyDrawn = false;
|
|
4374
|
+
for (const [slot, top] of bySlot) {
|
|
4375
|
+
const rev = Number(top?.data?.rev) || 0;
|
|
4376
|
+
if (rev > 0) anyDrawn = true;
|
|
4377
|
+
this.dispatch({
|
|
4378
|
+
type: "canvas/snapshot",
|
|
4379
|
+
sessionId,
|
|
4380
|
+
slot,
|
|
4381
|
+
rev,
|
|
4382
|
+
...(typeof top?.data?.name === "string" ? { name: top.data.name } : {}),
|
|
4383
|
+
note: typeof top?.data?.note === "string" ? top.data.note : "",
|
|
4384
|
+
sizeBytes: top?.data?.sizeBytes,
|
|
4385
|
+
responseContract: top?.data?.responseContract,
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
// The latest data tick per slot is that page's cold-load state —
|
|
4389
|
+
// replay it so a freshly loaded shell reconstructs without the
|
|
4390
|
+
// agent.
|
|
4391
|
+
if (anyDrawn) {
|
|
4355
4392
|
const dataRows = await this.transport.getSessionEventsBefore(
|
|
4356
|
-
sessionId, Number.MAX_SAFE_INTEGER,
|
|
4393
|
+
sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_data"],
|
|
4357
4394
|
).catch(() => []);
|
|
4358
|
-
const
|
|
4359
|
-
|
|
4395
|
+
const tickBySlot = new Map();
|
|
4396
|
+
for (const row of Array.isArray(dataRows) ? dataRows : []) {
|
|
4397
|
+
const slot = Number(row?.data?.slot) || 1;
|
|
4398
|
+
const dr = Number(row?.data?.dataRev) || 0;
|
|
4399
|
+
if (!tickBySlot.has(slot) || dr > (Number(tickBySlot.get(slot)?.data?.dataRev) || 0)) tickBySlot.set(slot, row);
|
|
4400
|
+
}
|
|
4401
|
+
for (const [slot, tick] of tickBySlot) {
|
|
4402
|
+
if (!tick?.data?.dataRev) continue;
|
|
4360
4403
|
this.dispatch({
|
|
4361
4404
|
type: "canvas/data",
|
|
4362
4405
|
sessionId,
|
|
4406
|
+
slot,
|
|
4363
4407
|
dataRev: Number(tick.data.dataRev),
|
|
4364
4408
|
payload: tick.data.payload,
|
|
4365
4409
|
note: typeof tick.data.note === "string" ? tick.data.note : "",
|
|
@@ -4371,6 +4415,27 @@ export class PilotSwarmUiController {
|
|
|
4371
4415
|
}
|
|
4372
4416
|
}
|
|
4373
4417
|
|
|
4418
|
+
/**
|
|
4419
|
+
* An agent asked to PRESENT an already-drawn canvas — no bytes, no rev,
|
|
4420
|
+
* nothing marked unseen. Same guards as a draw's auto-flip: only the
|
|
4421
|
+
* active session, only a fresh event (a reconnect replay must not yank
|
|
4422
|
+
* the view), and the user's manual dismissal of that slot still wins.
|
|
4423
|
+
*/
|
|
4424
|
+
applyCanvasPresented(sessionId, event) {
|
|
4425
|
+
const rawSlot = Number(event?.data?.slot);
|
|
4426
|
+
const slot = Number.isInteger(rawSlot) && rawSlot >= 1 && rawSlot <= 5 ? rawSlot : 1;
|
|
4427
|
+
if (!sessionId) return false;
|
|
4428
|
+
const state = this.getState();
|
|
4429
|
+
if (sessionId !== state.sessions.activeSessionId) return false;
|
|
4430
|
+
if (state.canvas.prefs[canvasPrefKey(sessionId, slot)]?.optedOut) return false;
|
|
4431
|
+
const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
|
|
4432
|
+
if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
|
|
4433
|
+
return false;
|
|
4434
|
+
}
|
|
4435
|
+
this.dispatch({ type: "canvas/flip", sessionId, slot });
|
|
4436
|
+
return true;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4374
4439
|
mergeSessionEvent(sessionId, event) {
|
|
4375
4440
|
if (!sessionId || !event) return false;
|
|
4376
4441
|
const state = this.getState();
|
|
@@ -4429,10 +4494,14 @@ export class PilotSwarmUiController {
|
|
|
4429
4494
|
if (event.eventType === "session.canvas_updated") {
|
|
4430
4495
|
this.applyCanvasUpdate(sessionId, event);
|
|
4431
4496
|
}
|
|
4497
|
+
if (event.eventType === "session.canvas_presented") {
|
|
4498
|
+
this.applyCanvasPresented(sessionId, event);
|
|
4499
|
+
}
|
|
4432
4500
|
if (event.eventType === "session.canvas_data") {
|
|
4433
4501
|
this.dispatch({
|
|
4434
4502
|
type: "canvas/data",
|
|
4435
4503
|
sessionId,
|
|
4504
|
+
slot: Number(event?.data?.slot) || 1,
|
|
4436
4505
|
dataRev: Number(event?.data?.dataRev),
|
|
4437
4506
|
payload: event?.data?.payload,
|
|
4438
4507
|
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
@@ -7449,7 +7518,15 @@ export class PilotSwarmUiController {
|
|
|
7449
7518
|
|
|
7450
7519
|
applyPaneVisualScrollOffset(pane, offset, options = {}, state = this.getState()) {
|
|
7451
7520
|
const maxOffset = this.getPaneMaxScrollOffset(pane, state);
|
|
7452
|
-
|
|
7521
|
+
// fromViewport: the offset was measured off the real DOM (scrollTop /
|
|
7522
|
+
// row height), which the browser has already bounded. Clamping it
|
|
7523
|
+
// against the CONTROLLER's max — counted in TUI render-metric rows, a
|
|
7524
|
+
// different unit — teleported the pane on the first wheel tick: one
|
|
7525
|
+
// 100px notch re-applied as metric-max × 16px, thousands of pixels
|
|
7526
|
+
// away. The two row spaces must never clamp each other.
|
|
7527
|
+
const nextOffset = options.fromViewport
|
|
7528
|
+
? Math.max(0, Number(offset) || 0)
|
|
7529
|
+
: Math.max(0, Math.min(Number(offset) || 0, maxOffset));
|
|
7453
7530
|
if (this.paneUsesStickyBottomFollow(pane, state)) {
|
|
7454
7531
|
const followBottom = options.followBottom !== undefined
|
|
7455
7532
|
? Boolean(options.followBottom)
|
|
@@ -7468,7 +7545,7 @@ export class PilotSwarmUiController {
|
|
|
7468
7545
|
updatePaneScrollFromViewport(pane, offset, options = {}) {
|
|
7469
7546
|
const state = this.getState();
|
|
7470
7547
|
if (this.paneUsesStickyBottomFollow(pane, state)) {
|
|
7471
|
-
this.applyPaneVisualScrollOffset(pane, offset, options, state);
|
|
7548
|
+
this.applyPaneVisualScrollOffset(pane, offset, { ...options, fromViewport: true }, state);
|
|
7472
7549
|
return;
|
|
7473
7550
|
}
|
|
7474
7551
|
this.scrollPaneTo(pane, offset);
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
normalizeSessionOwnerFilter,
|
|
10
10
|
normalizeStoredActiveSessionId,
|
|
11
11
|
normalizeStoredCanvasPrefs,
|
|
12
|
+
canvasKey,
|
|
13
|
+
parseCanvasKey,
|
|
12
14
|
normalizeStoredCollapsedSessionIds,
|
|
15
|
+
normalizeStoredDesktopPanes,
|
|
13
16
|
normalizeStoredLayoutAdjustments,
|
|
14
17
|
normalizeStoredPinnedSessionIds,
|
|
15
18
|
normalizeStoredSessionOrder,
|
|
@@ -112,10 +115,15 @@ function mergeCanvasPrefs(local, stored, byId) {
|
|
|
112
115
|
optedOut: storedEntry ? storedEntry.optedOut : Boolean(localEntry?.optedOut),
|
|
113
116
|
lastViewedRev: Math.max(storedEntry?.lastViewedRev || 0, localEntry?.lastViewedRev || 0),
|
|
114
117
|
lastViewedDataRev: Math.max(storedEntry?.lastViewedDataRev || 0, localEntry?.lastViewedDataRev || 0),
|
|
118
|
+
// Latest write wins is unknowable here; the local value is the one
|
|
119
|
+
// this window just dragged, so it outranks the poll.
|
|
120
|
+
...((localEntry?.zenRailPx || storedEntry?.zenRailPx)
|
|
121
|
+
? { zenRailPx: localEntry?.zenRailPx || storedEntry?.zenRailPx }
|
|
122
|
+
: {}),
|
|
115
123
|
};
|
|
116
|
-
if (!merged.optedOut && merged.lastViewedRev === 0 && merged.lastViewedDataRev === 0) continue;
|
|
117
|
-
const session = byId?.[sessionId];
|
|
118
|
-
if (session &&
|
|
124
|
+
if (!merged.optedOut && merged.lastViewedRev === 0 && merged.lastViewedDataRev === 0 && !merged.zenRailPx) continue;
|
|
125
|
+
const session = byId?.[parseCanvasKey(sessionId).sessionId];
|
|
126
|
+
if (session && session.isGroup) continue;
|
|
119
127
|
out[sessionId] = merged;
|
|
120
128
|
}
|
|
121
129
|
return out;
|
|
@@ -808,6 +816,17 @@ export function appReducer(state, action) {
|
|
|
808
816
|
&& typeof settings.touchScale === "boolean";
|
|
809
817
|
const hasRightPaneMode = Object.prototype.hasOwnProperty.call(settings, "rightPaneMode")
|
|
810
818
|
&& (settings.rightPaneMode === "canvas" || settings.rightPaneMode === "panes");
|
|
819
|
+
// The two independent columns. A profile written by a build that
|
|
820
|
+
// predates them carries only rightPaneMode, so fall back to
|
|
821
|
+
// migrating that — see normalizeStoredDesktopPanes.
|
|
822
|
+
const hasDesktopPanes = Object.prototype.hasOwnProperty.call(settings, "desktopPanes")
|
|
823
|
+
&& settings.desktopPanes && typeof settings.desktopPanes === "object";
|
|
824
|
+
const nextDesktopPanes = (hasDesktopPanes || hasRightPaneMode)
|
|
825
|
+
? normalizeStoredDesktopPanes(
|
|
826
|
+
hasDesktopPanes ? settings.desktopPanes : null,
|
|
827
|
+
hasRightPaneMode ? settings.rightPaneMode : null,
|
|
828
|
+
)
|
|
829
|
+
: null;
|
|
811
830
|
const hasCanvasPrefs = Object.prototype.hasOwnProperty.call(settings, "canvasPrefs");
|
|
812
831
|
const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
|
|
813
832
|
const hasCollapsed = Object.prototype.hasOwnProperty.call(settings, "collapsedSessionIds");
|
|
@@ -872,6 +891,9 @@ export function appReducer(state, action) {
|
|
|
872
891
|
themeId: hasTheme ? settings.themeId.trim() : selection.ui.themeId,
|
|
873
892
|
touchScale: hasTouchScale ? settings.touchScale : selection.ui.touchScale,
|
|
874
893
|
rightPaneMode: hasRightPaneMode ? settings.rightPaneMode : selection.ui.rightPaneMode,
|
|
894
|
+
...(nextDesktopPanes
|
|
895
|
+
? { canvasOpen: nextDesktopPanes.canvasOpen, diagnosticsOpen: nextDesktopPanes.diagnosticsOpen, canvasZen: nextDesktopPanes.zen === true }
|
|
896
|
+
: {}),
|
|
875
897
|
layout: nextLayout,
|
|
876
898
|
},
|
|
877
899
|
canvas: hasCanvasPrefs
|
|
@@ -946,6 +968,46 @@ export function appReducer(state, action) {
|
|
|
946
968
|
},
|
|
947
969
|
};
|
|
948
970
|
|
|
971
|
+
// Where the canvas/diagnostics seam sits when both columns are open.
|
|
972
|
+
// A pixel delta from an even split of the right side, like the other
|
|
973
|
+
// adjusts. Closing a column is a separate action — see
|
|
974
|
+
// CanvasDiagnosticsResizeHandle, which decides that on release.
|
|
975
|
+
case "ui/canvasPaneAdjust":
|
|
976
|
+
return {
|
|
977
|
+
...state,
|
|
978
|
+
ui: {
|
|
979
|
+
...state.ui,
|
|
980
|
+
layout: {
|
|
981
|
+
...(state.ui.layout || {}),
|
|
982
|
+
canvasPaneAdjust: Number(action.canvasPaneAdjust) || 0,
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
case "ui/diagnosticsSplitAdjust":
|
|
988
|
+
return {
|
|
989
|
+
...state,
|
|
990
|
+
ui: {
|
|
991
|
+
...state.ui,
|
|
992
|
+
layout: {
|
|
993
|
+
...(state.ui.layout || {}),
|
|
994
|
+
diagnosticsSplitAdjust: Number(action.diagnosticsSplitAdjust) || 0,
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
case "ui/diagnosticsPaneAdjust":
|
|
1000
|
+
return {
|
|
1001
|
+
...state,
|
|
1002
|
+
ui: {
|
|
1003
|
+
...state.ui,
|
|
1004
|
+
layout: {
|
|
1005
|
+
...(state.ui.layout || {}),
|
|
1006
|
+
diagnosticsPaneAdjust: Number(action.diagnosticsPaneAdjust) || 0,
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
|
|
949
1011
|
case "ui/activityPaneAdjust":
|
|
950
1012
|
return {
|
|
951
1013
|
...state,
|
|
@@ -2638,24 +2700,114 @@ export function appReducer(state, action) {
|
|
|
2638
2700
|
}
|
|
2639
2701
|
return {
|
|
2640
2702
|
...state,
|
|
2641
|
-
ui: { ...state.ui, rightPaneMode: mode },
|
|
2703
|
+
ui: { ...state.ui, rightPaneMode: mode, canvasOpen: mode === "canvas" },
|
|
2704
|
+
canvas: { ...state.canvas, prefs },
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// Canvas as a column of its own. Same opt-out bookkeeping as the old
|
|
2709
|
+
// mode switch above — closing by hand still says "do not flip me back",
|
|
2710
|
+
// opening by hand still withdraws that — but closing the canvas no
|
|
2711
|
+
// longer implies showing anything else, because Diagnostics is now a
|
|
2712
|
+
// separate column with its own toggle.
|
|
2713
|
+
//
|
|
2714
|
+
// `open` may be omitted to mean "toggle".
|
|
2715
|
+
case "ui/canvasOpen": {
|
|
2716
|
+
const open = typeof action.open === "boolean" ? action.open : !state.ui.canvasOpen;
|
|
2717
|
+
const sessionId = action.sessionId || state.sessions.activeSessionId || null;
|
|
2718
|
+
let prefs = state.canvas.prefs;
|
|
2719
|
+
if (sessionId) {
|
|
2720
|
+
const prev = prefs[sessionId] || { optedOut: false, lastViewedRev: 0 };
|
|
2721
|
+
if (open && prev.optedOut) {
|
|
2722
|
+
prefs = { ...prefs, [sessionId]: { ...prev, optedOut: false } };
|
|
2723
|
+
} else if (!open && action.manual && !prev.optedOut) {
|
|
2724
|
+
prefs = { ...prefs, [sessionId]: { ...prev, optedOut: true } };
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
...state,
|
|
2729
|
+
// rightPaneMode is kept in step for anything still reading it.
|
|
2730
|
+
// Closing always drops full screen: otherwise the next open
|
|
2731
|
+
// comes back covering the workspace with no warning.
|
|
2732
|
+
ui: {
|
|
2733
|
+
...state.ui,
|
|
2734
|
+
canvasOpen: open,
|
|
2735
|
+
rightPaneMode: open ? "canvas" : "panes",
|
|
2736
|
+
...(open ? {} : { canvasMaximized: false, canvasZen: false }),
|
|
2737
|
+
},
|
|
2642
2738
|
canvas: { ...state.canvas, prefs },
|
|
2643
2739
|
};
|
|
2644
2740
|
}
|
|
2645
2741
|
|
|
2742
|
+
// Diagnostics — today's Inspector + Activity, travelling as one column.
|
|
2743
|
+
// No canvas bookkeeping: it has nothing to do with the canvas, which is
|
|
2744
|
+
// the whole point of splitting the old enum in two.
|
|
2745
|
+
// Zen: sessions hidden, chat a narrow rail, canvas the workbench.
|
|
2746
|
+
// Turning it on opens the canvas; it means nothing without one.
|
|
2747
|
+
case "ui/canvasZen": {
|
|
2748
|
+
const on = typeof action.on === "boolean" ? action.on : !state.ui.canvasZen;
|
|
2749
|
+
if (on === Boolean(state.ui.canvasZen)) return state;
|
|
2750
|
+
return {
|
|
2751
|
+
...state,
|
|
2752
|
+
ui: { ...state.ui, canvasZen: on, ...(on ? { canvasOpen: true, rightPaneMode: "canvas" } : {}) },
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// The zen chat-rail width for one session. Clamped so a wild drag can
|
|
2757
|
+
// never wedge the rail off screen or crush the canvas.
|
|
2758
|
+
case "canvas/zenRail": {
|
|
2759
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
2760
|
+
const px = Number(action.px);
|
|
2761
|
+
if (!sessionId || !Number.isFinite(px)) return state;
|
|
2762
|
+
const prev = state.canvas.prefs[sessionId] || { optedOut: false, lastViewedRev: 0 };
|
|
2763
|
+
const clamped = Math.max(260, Math.min(720, Math.round(px)));
|
|
2764
|
+
return {
|
|
2765
|
+
...state,
|
|
2766
|
+
canvas: { ...state.canvas, prefs: { ...state.canvas.prefs, [sessionId]: { ...prev, zenRailPx: clamped } } },
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// Full-screen canvas. The header stays put and the canvas covers the
|
|
2771
|
+
// workspace below it, so there is always a way back.
|
|
2772
|
+
case "ui/canvasMaximized": {
|
|
2773
|
+
const on = typeof action.on === "boolean" ? action.on : !state.ui.canvasMaximized;
|
|
2774
|
+
if (on === Boolean(state.ui.canvasMaximized)) return state;
|
|
2775
|
+
return { ...state, ui: { ...state.ui, canvasMaximized: on } };
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
case "ui/diagnosticsOpen": {
|
|
2779
|
+
const open = typeof action.open === "boolean" ? action.open : !state.ui.diagnosticsOpen;
|
|
2780
|
+
if (open === state.ui.diagnosticsOpen) return state;
|
|
2781
|
+
return { ...state, ui: { ...state.ui, diagnosticsOpen: open } };
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2646
2784
|
// The agent-driven flip. The guards (active session, freshness,
|
|
2647
2785
|
// opt-out) live in the controller; by the time this dispatches the
|
|
2648
2786
|
// flip is decided. flipSeq ticks even when the mode is already
|
|
2649
2787
|
// "canvas" — a phone can be off the canvas tab while the persisted
|
|
2650
2788
|
// mode still says canvas, and the tick is what brings its tab back.
|
|
2651
2789
|
case "canvas/flip": {
|
|
2790
|
+
const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
|
|
2791
|
+
? Number(action.slot) : 1;
|
|
2652
2792
|
return {
|
|
2653
2793
|
...state,
|
|
2654
|
-
|
|
2794
|
+
// Opens the canvas column ON THE SLOT THAT DREW; leaves
|
|
2795
|
+
// Diagnostics exactly as the user left it. An agent drawing
|
|
2796
|
+
// must not close their panels.
|
|
2797
|
+
ui: { ...state.ui, rightPaneMode: "canvas", canvasOpen: true, canvasSlot: slot },
|
|
2655
2798
|
canvas: { ...state.canvas, flipSeq: (state.canvas.flipSeq || 0) + 1 },
|
|
2656
2799
|
};
|
|
2657
2800
|
}
|
|
2658
2801
|
|
|
2802
|
+
// Which of the active session's canvases the pane shows. In-memory
|
|
2803
|
+
// only: a reload lands on slot 1 (or wherever the next flip points).
|
|
2804
|
+
case "ui/canvasSlot": {
|
|
2805
|
+
const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
|
|
2806
|
+
? Number(action.slot) : 1;
|
|
2807
|
+
if (slot === (state.ui.canvasSlot || 1)) return state;
|
|
2808
|
+
return { ...state, ui: { ...state.ui, canvasSlot: slot } };
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2659
2811
|
// The surface's receipt: revision `rev` (a draw) and/or `dataRev` (a
|
|
2660
2812
|
// tick) actually rendered on screen. Both feed the unseen badge.
|
|
2661
2813
|
case "canvas/viewed": {
|
|
@@ -2663,7 +2815,8 @@ export function appReducer(state, action) {
|
|
|
2663
2815
|
if (!sessionId) return state;
|
|
2664
2816
|
const rev = Number(action.rev);
|
|
2665
2817
|
const dataRev = Number(action.dataRev);
|
|
2666
|
-
const
|
|
2818
|
+
const key = canvasKey(sessionId, action.slot);
|
|
2819
|
+
const prev = state.canvas.prefs[key] || { optedOut: false, lastViewedRev: 0, lastViewedDataRev: 0 };
|
|
2667
2820
|
const nextRev = Number.isFinite(rev) && rev > (prev.lastViewedRev || 0) ? rev : (prev.lastViewedRev || 0);
|
|
2668
2821
|
const nextDataRev = Number.isFinite(dataRev) && dataRev > (prev.lastViewedDataRev || 0) ? dataRev : (prev.lastViewedDataRev || 0);
|
|
2669
2822
|
if (nextRev === (prev.lastViewedRev || 0) && nextDataRev === (prev.lastViewedDataRev || 0)) return state;
|
|
@@ -2671,16 +2824,49 @@ export function appReducer(state, action) {
|
|
|
2671
2824
|
...state,
|
|
2672
2825
|
canvas: {
|
|
2673
2826
|
...state.canvas,
|
|
2674
|
-
prefs: { ...state.canvas.prefs, [
|
|
2675
|
-
},
|
|
2676
|
-
};
|
|
2827
|
+
prefs: { ...state.canvas.prefs, [key]: { ...prev, lastViewedRev: nextRev, lastViewedDataRev: nextDataRev } },
|
|
2828
|
+
},
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// The sessions-list seed: rev+name per slot from session_canvases,
|
|
2833
|
+
// merged under the live entries. Never regresses a rev a live event
|
|
2834
|
+
// already delivered, never sets snapshotLoaded (the full snapshot —
|
|
2835
|
+
// note, contract, data replay — still loads on selection).
|
|
2836
|
+
case "canvas/seed": {
|
|
2837
|
+
const entries = Array.isArray(action.entries) ? action.entries : [];
|
|
2838
|
+
if (!entries.length) return state;
|
|
2839
|
+
let next = null;
|
|
2840
|
+
for (const e of entries) {
|
|
2841
|
+
const sessionId = String(e?.sessionId || "").trim();
|
|
2842
|
+
const rev = Number(e?.latestRev);
|
|
2843
|
+
const slotN = Number(e?.slot);
|
|
2844
|
+
if (!sessionId || !Number.isFinite(rev) || rev <= 0) continue;
|
|
2845
|
+
if (!Number.isInteger(slotN) || slotN < 1 || slotN > 5) continue;
|
|
2846
|
+
const key = canvasKey(sessionId, slotN);
|
|
2847
|
+
const existing = (next || state.canvas.bySessionId)[key] || {};
|
|
2848
|
+
if ((existing.latestRev || 0) >= rev && existing.name !== undefined) continue;
|
|
2849
|
+
next = next || { ...state.canvas.bySessionId };
|
|
2850
|
+
next[key] = {
|
|
2851
|
+
...existing,
|
|
2852
|
+
slot: slotN,
|
|
2853
|
+
latestRev: Math.max(existing.latestRev || 0, rev),
|
|
2854
|
+
...(typeof e.name === "string" && existing.name === undefined ? { name: e.name } : {}),
|
|
2855
|
+
...(existing.sizeBytes === undefined && e.sizeBytes !== undefined ? { sizeBytes: e.sizeBytes } : {}),
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
if (!next) return state;
|
|
2859
|
+
return { ...state, canvas: { ...state.canvas, bySessionId: next } };
|
|
2677
2860
|
}
|
|
2678
2861
|
|
|
2679
2862
|
case "canvas/updated": {
|
|
2680
2863
|
const sessionId = String(action.sessionId || "").trim();
|
|
2681
2864
|
const rev = Number(action.rev);
|
|
2682
2865
|
if (!sessionId || !Number.isFinite(rev) || rev <= 0) return state;
|
|
2683
|
-
const
|
|
2866
|
+
const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
|
|
2867
|
+
? Number(action.slot) : 1;
|
|
2868
|
+
const key = canvasKey(sessionId, slot);
|
|
2869
|
+
const existing = state.canvas.bySessionId[key] || {};
|
|
2684
2870
|
// Monotonic: replays and out-of-order merges must never regress
|
|
2685
2871
|
// the known revision (freshness gates the FLIP, never the data).
|
|
2686
2872
|
if ((existing.latestRev || 0) >= rev) return state;
|
|
@@ -2690,8 +2876,10 @@ export function appReducer(state, action) {
|
|
|
2690
2876
|
...state.canvas,
|
|
2691
2877
|
bySessionId: {
|
|
2692
2878
|
...state.canvas.bySessionId,
|
|
2693
|
-
[
|
|
2879
|
+
[key]: {
|
|
2694
2880
|
...existing,
|
|
2881
|
+
slot,
|
|
2882
|
+
...(typeof action.name === "string" ? { name: action.name } : {}),
|
|
2695
2883
|
latestRev: rev,
|
|
2696
2884
|
note: typeof action.note === "string" ? action.note : "",
|
|
2697
2885
|
sizeBytes: Number.isFinite(Number(action.sizeBytes)) ? Number(action.sizeBytes) : null,
|
|
@@ -2712,9 +2900,12 @@ export function appReducer(state, action) {
|
|
|
2712
2900
|
// latestDataRev vs lastViewedDataRev. Monotonic like the draw rev.
|
|
2713
2901
|
case "canvas/data": {
|
|
2714
2902
|
const sessionId = String(action.sessionId || "").trim();
|
|
2903
|
+
const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
|
|
2904
|
+
? Number(action.slot) : 1;
|
|
2905
|
+
const key = canvasKey(sessionId, slot);
|
|
2715
2906
|
const dataRev = Number(action.dataRev);
|
|
2716
2907
|
if (!sessionId || !Number.isFinite(dataRev) || dataRev <= 0) return state;
|
|
2717
|
-
const existing = state.canvas.bySessionId[
|
|
2908
|
+
const existing = state.canvas.bySessionId[key] || {};
|
|
2718
2909
|
if ((existing.latestDataRev || 0) >= dataRev) return state;
|
|
2719
2910
|
return {
|
|
2720
2911
|
...state,
|
|
@@ -2722,7 +2913,7 @@ export function appReducer(state, action) {
|
|
|
2722
2913
|
...state.canvas,
|
|
2723
2914
|
bySessionId: {
|
|
2724
2915
|
...state.canvas.bySessionId,
|
|
2725
|
-
[
|
|
2916
|
+
[key]: {
|
|
2726
2917
|
...existing,
|
|
2727
2918
|
latestDataRev: dataRev,
|
|
2728
2919
|
dataPayload: action.payload && typeof action.payload === "object" ? action.payload : null,
|
|
@@ -2735,7 +2926,10 @@ export function appReducer(state, action) {
|
|
|
2735
2926
|
case "canvas/snapshot": {
|
|
2736
2927
|
const sessionId = String(action.sessionId || "").trim();
|
|
2737
2928
|
if (!sessionId) return state;
|
|
2738
|
-
const
|
|
2929
|
+
const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
|
|
2930
|
+
? Number(action.slot) : 1;
|
|
2931
|
+
const key = canvasKey(sessionId, slot);
|
|
2932
|
+
const existing = state.canvas.bySessionId[key] || {};
|
|
2739
2933
|
const rev = Number(action.rev) || 0;
|
|
2740
2934
|
return {
|
|
2741
2935
|
...state,
|
|
@@ -2743,8 +2937,10 @@ export function appReducer(state, action) {
|
|
|
2743
2937
|
...state.canvas,
|
|
2744
2938
|
bySessionId: {
|
|
2745
2939
|
...state.canvas.bySessionId,
|
|
2746
|
-
[
|
|
2940
|
+
[key]: {
|
|
2747
2941
|
...existing,
|
|
2942
|
+
slot,
|
|
2943
|
+
...(typeof action.name === "string" && ((existing.latestRev || 0) <= rev) ? { name: action.name } : {}),
|
|
2748
2944
|
// Events may have beaten the snapshot; never regress.
|
|
2749
2945
|
latestRev: Math.max(existing.latestRev || 0, rev),
|
|
2750
2946
|
note: (existing.latestRev || 0) > rev ? existing.note : (typeof action.note === "string" ? action.note : existing.note || ""),
|
|
@@ -2765,6 +2961,8 @@ export function appReducer(state, action) {
|
|
|
2765
2961
|
const sessionId = String(action.sessionId || "").trim();
|
|
2766
2962
|
const existing = state.canvas.bySessionId[sessionId];
|
|
2767
2963
|
if (!sessionId || !existing?.snapshotLoaded) return state;
|
|
2964
|
+
// Slots 2-5 of the same session invalidate with slot 1: the flag
|
|
2965
|
+
// that gates re-take lives on the slot-1 entry.
|
|
2768
2966
|
return {
|
|
2769
2967
|
...state,
|
|
2770
2968
|
canvas: {
|