pilotswarm 0.5.39 → 0.5.40
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 +2 -2
- package/tui/src/node-sdk-transport.js +25 -0
- package/ui/core/src/controller.js +34 -8
- package/ui/core/src/history.js +5 -1
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/portal-links.js +46 -0
- package/ui/core/src/reducer.js +88 -2
- package/ui/core/src/selectors.js +2 -0
- package/ui/react/src/web-app.js +369 -34
- package/web/README.md +28 -0
- package/web/api/canvas-plane.js +116 -0
- package/web/api/ws.js +120 -5
- package/web/config.js +60 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-Hsly1fgf.js → abnfDiagram-VRR7QNED-C6FaYMB7.js} +1 -1
- package/web/dist/assets/{arc-DiQLL9vM.js → arc-iZBUHcPs.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js → architectureDiagram-ZJ3FMSHR-D4-wnpAG.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Z4PcukIo.js → blockDiagram-677ZJIJ3-Dz-s103f.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Cahkc0gc.js → c4Diagram-LMCZKHZV-DE7sGdHE.js} +1 -1
- package/web/dist/assets/channel-Bk68hLPV.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CtZuWH4J.js → chunk-2Q5K7J3B-DSFs_jRM.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C0M390-k.js → chunk-32BRIVSS-8sBVtZ4f.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-ClvX9u5j.js → chunk-5VM5RSS4-CbIX7k0G.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BVAet5KG.js → chunk-EX3LRPZG-ZfmPtWF8.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BccmZmrr.js → chunk-JWPE2WC7-DifZrqsQ.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BN5MKe94.js → chunk-MOJQB5TN-CEvg_lZT.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DfWw-ACP.js → chunk-RYQCIY6F-DmYOwk6-.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-BgUEoLnN.js → chunk-V7JOEXUC-DOxG9Vxu.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-k1eKfFNA.js → chunk-VR4S4FIN-ic-KB2tF.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DPyV10yR.js → chunk-XXDRQBXY-BqeYnem3.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-arTJL-2y.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-arTJL-2y.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bt_rpukK.js → cose-bilkent-JH36ORCC-uaIgs2-N.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-Z8eMzFMv.js → cynefin-VYW2F7L2-DzFeugVQ.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BJSNawFi.js → cynefinDiagram-TSTJHNR4-NrAChuCW.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-D6dNl8jc.js → dagre-VKFMJZFB-Djf9LD2t.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-Cy1nbGsU.js → diagram-FQU43EPY-BpIP2iD9.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CmrbuA_d.js → diagram-G47NLZAW-D_DCw8Oy.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-PoLNQE2j.js → diagram-NH7WQ7WH-BztvRvrM.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BZwNFcSV.js → diagram-OA4YK3LP-BGIwSnbM.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-pKw0b_fN.js → diagram-WEI45ONY-Czj2ttCt.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-UNctuQCV.js → ebnfDiagram-CCIWWBDH-0nqAhMvJ.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DxIETMif.js → erDiagram-Q63AITRT-uLoGb9TG.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-C06IdNKV.js → flowDiagram-23GEKE2U-Bgxy7CWO.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-fU7aGyP1.js → ganttDiagram-NO4QXBWP-5q8zYzbu.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-9UKkPJm6.js → gitGraphDiagram-IHSO6WYX-D6-i59JZ.js} +1 -1
- package/web/dist/assets/index-B3WlYEvi.css +1 -0
- package/web/dist/assets/index-BFa_e8zL.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-C6F06SQJ.js → infoDiagram-FWYZ7A6U-bdx9GrWW.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CB-O9NfO.js → ishikawaDiagram-FXEZZL3T-xzL6Bssj.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B-8MOf-H.js → journeyDiagram-5HDEW3XC-C1-18xOk.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DX0XA3hI.js → kanban-definition-HUTT4EX6-BS4KDL4_.js} +1 -1
- package/web/dist/assets/{linear-D60PrjTW.js → linear-Dmv3ton3.js} +1 -1
- package/web/dist/assets/{mermaid.core-bonzwapf.js → mermaid.core-Nhzw7Fl2.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-Y-6Dum33.js → mindmap-definition-LN4V7U3C-2DSMdlsp.js} +1 -1
- package/web/dist/assets/{papaparse.min-Cas1JfRG.js → papaparse.min-H4g6q6aH.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-_paO0yDw.js → pegDiagram-2B236MQR-CM_rgJ2s.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-xT1_Gpn4.js → pieDiagram-ENE6RG2P-Cbo8FtuT.js} +1 -1
- package/web/dist/assets/pilotswarm-ClLipXts.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-1usdLFOF.js → quadrantDiagram-ABIIQ3AL-CVsF_pZA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-R3lwHOz9.js → railroadDiagram-RFXS5EU6-CauKYIfy.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BL8J1__Z.js → requirementDiagram-TGXJPOKE-DoXh8EL8.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Dz6--0pl.js → sankeyDiagram-HTMAVEWB-gA_ej2MB.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CG2Xpkct.js → sequenceDiagram-DBY2YBRQ-keYBa83Q.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-BPbYfjxj.js → sizeCapture-X5ZJPWSS-Y6NCdqw8.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-46x_y_aA.js → stateDiagram-2N3HPSRC-MvJfZXqW.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-xTZh36yB.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-SXdMo4zJ.js → swimlanes-5IMT3BWC-D41i7zcr.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BjlBuuiD.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-CGgT-b38.js → timeline-definition-FHXFAJF6-CUA2ENV8.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BGS3xBa_.js → vennDiagram-L72KCM5P-CBTp-2d6.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-m2U3wEZJ.js → wardleyDiagram-EHGQE667-BIFjtpCg.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DI6tQysg.js → xychartDiagram-FW5EYKEG-DiamOwOI.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +71 -0
- package/web/server.js +51 -2
- package/web/dist/assets/channel-BHLBPE50.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-rj9D2TSL.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-rj9D2TSL.js +0 -1
- package/web/dist/assets/index-B8nGlHwb.js +0 -24
- package/web/dist/assets/index-DtL0lbis.css +0 -1
- package/web/dist/assets/pilotswarm-BT6tOknQ.js +0 -96
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C2tnolSo.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Dh6Sn__t.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.40",
|
|
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,7 +81,7 @@
|
|
|
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.40",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
@@ -2111,6 +2111,31 @@ export class NodeSdkTransport {
|
|
|
2111
2111
|
return this.mgmt.getSessionEventsBefore(sessionId, beforeSeq, limit, eventTypes);
|
|
2112
2112
|
}
|
|
2113
2113
|
|
|
2114
|
+
async getCanvasLive(sessionId) {
|
|
2115
|
+
if (typeof this.mgmt.getCanvasLive !== "function") return [];
|
|
2116
|
+
return this.mgmt.getCanvasLive(sessionId);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async getCanvasShareLink(sessionId, slot) {
|
|
2120
|
+
if (typeof this.mgmt.getCanvasShareLink !== "function") return { exists: false };
|
|
2121
|
+
return this.mgmt.getCanvasShareLink(sessionId, slot);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
async resetCanvasShareLink(sessionId, slot, createdBy) {
|
|
2125
|
+
if (typeof this.mgmt.resetCanvasShareLink !== "function") throw new Error("canvas share links unavailable");
|
|
2126
|
+
return this.mgmt.resetCanvasShareLink(sessionId, slot, createdBy);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
async resolveCanvasShareTokenHash(tokenHash) {
|
|
2130
|
+
if (typeof this.mgmt.resolveCanvasShareTokenHash !== "function") return null;
|
|
2131
|
+
return this.mgmt.resolveCanvasShareTokenHash(tokenHash);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
async removeCanvasShareLink(sessionId, slot) {
|
|
2135
|
+
if (typeof this.mgmt.removeCanvasShareLink !== "function") return { removed: false };
|
|
2136
|
+
return this.mgmt.removeCanvasShareLink(sessionId, slot);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2114
2139
|
emitLogEntry(entry) {
|
|
2115
2140
|
if (!this._logBatch) this._logBatch = [];
|
|
2116
2141
|
this._logBatch.push(entry);
|
|
@@ -4499,8 +4499,41 @@ export class PilotSwarmUiController {
|
|
|
4499
4499
|
return true;
|
|
4500
4500
|
}
|
|
4501
4501
|
|
|
4502
|
+
applyCanvasDataEvent(sessionId, event) {
|
|
4503
|
+
this.dispatch({
|
|
4504
|
+
type: "canvas/data",
|
|
4505
|
+
sessionId,
|
|
4506
|
+
slot: Number(event?.data?.slot) || 1,
|
|
4507
|
+
dataRev: Number(event?.data?.dataRev),
|
|
4508
|
+
payload: event?.data?.payload,
|
|
4509
|
+
// Plane-fed ticks (the canvas data plane's live mirror) carry
|
|
4510
|
+
// their own seq lineage and the patch that produced them — the
|
|
4511
|
+
// reducer orders plane ticks by planeSeq and hands the patch to
|
|
4512
|
+
// pages that opt into targeted updates.
|
|
4513
|
+
...(Number.isFinite(Number(event?.data?.planeSeq)) ? { planeSeq: Number(event.data.planeSeq) } : {}),
|
|
4514
|
+
...(event?.data?.patch && typeof event.data.patch === "object" ? { patch: event.data.patch } : {}),
|
|
4515
|
+
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4502
4519
|
mergeSessionEvent(sessionId, event) {
|
|
4503
4520
|
if (!sessionId || !event) return false;
|
|
4521
|
+
// Plane-synthesized canvas events are TRANSIENT: they update canvas
|
|
4522
|
+
// state and nothing else. They carry no seq — letting them into
|
|
4523
|
+
// history would corrupt the replay cursor — and they must never
|
|
4524
|
+
// appear in the transcript.
|
|
4525
|
+
if (event.transient === true) {
|
|
4526
|
+
if (event.eventType === "session.canvas_data") {
|
|
4527
|
+
this.applyCanvasDataEvent(sessionId, event);
|
|
4528
|
+
}
|
|
4529
|
+
if (event.eventType === "session.canvas_plane_released") {
|
|
4530
|
+
// The plane died for this session (server error/rollback).
|
|
4531
|
+
// Clear the reducer's takeover so legacy durable ticks — the
|
|
4532
|
+
// fallback path — apply again instead of being dropped.
|
|
4533
|
+
this.dispatch({ type: "canvas/planeReleased", sessionId });
|
|
4534
|
+
}
|
|
4535
|
+
return false;
|
|
4536
|
+
}
|
|
4504
4537
|
const state = this.getState();
|
|
4505
4538
|
const existing = state.history.bySessionId.get(sessionId) || { chat: [], activity: [], lastSeq: 0 };
|
|
4506
4539
|
if (event.seq <= (existing.lastSeq || 0)) return false;
|
|
@@ -4561,14 +4594,7 @@ export class PilotSwarmUiController {
|
|
|
4561
4594
|
this.applyCanvasPresented(sessionId, event);
|
|
4562
4595
|
}
|
|
4563
4596
|
if (event.eventType === "session.canvas_data") {
|
|
4564
|
-
this.
|
|
4565
|
-
type: "canvas/data",
|
|
4566
|
-
sessionId,
|
|
4567
|
-
slot: Number(event?.data?.slot) || 1,
|
|
4568
|
-
dataRev: Number(event?.data?.dataRev),
|
|
4569
|
-
payload: event?.data?.payload,
|
|
4570
|
-
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
4571
|
-
});
|
|
4597
|
+
this.applyCanvasDataEvent(sessionId, event);
|
|
4572
4598
|
}
|
|
4573
4599
|
this.reconcileOutboxAgainstEvent(sessionId, event);
|
|
4574
4600
|
const derivedModel = extractSessionModelFromEvent(event);
|
package/ui/core/src/history.js
CHANGED
|
@@ -1123,7 +1123,11 @@ export function appendEventToHistory(history, event) {
|
|
|
1123
1123
|
chat: clampHistoryItems(history?.chat || [], loadedEventLimit),
|
|
1124
1124
|
activity: clampHistoryItems(history?.activity || [], loadedEventLimit),
|
|
1125
1125
|
events: nextEvents,
|
|
1126
|
-
|
|
1126
|
+
// The replay cursor. A seq-less event (plane-synthesized canvas
|
|
1127
|
+
// ticks are transient and carry none) must never poison it —
|
|
1128
|
+
// lastSeq=undefined reads as afterSeq 0 on the next sync and the
|
|
1129
|
+
// whole transcript re-merges, duplicated.
|
|
1130
|
+
lastSeq: Number.isFinite(Number(event.seq)) ? event.seq : (history?.lastSeq || 0),
|
|
1127
1131
|
loadedEventLimit,
|
|
1128
1132
|
loadedEventCount: Math.max(Number(history?.loadedEventCount || 0), nextEvents.length),
|
|
1129
1133
|
hasOlderEvents: Boolean(history?.hasOlderEvents),
|
package/ui/core/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./repo-import.js";
|
|
|
8
8
|
export * from "./session-tree.js";
|
|
9
9
|
export * from "./state.js";
|
|
10
10
|
export * from "./reducer.js";
|
|
11
|
+
export * from "./portal-links.js";
|
|
11
12
|
export * from "./store.js";
|
|
12
13
|
export * from "./selectors.js";
|
|
13
14
|
export * from "./controller.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-origin portal links.
|
|
3
|
+
*
|
|
4
|
+
* A deployment can be reachable through several entry points at once — a
|
|
5
|
+
* public edge and a private/VPN hostname, say — and a link built from the
|
|
6
|
+
* sender's window.location.origin only works for recipients on the SAME
|
|
7
|
+
* network path. When the portal is configured with named link origins,
|
|
8
|
+
* every link-producing surface offers one variant per origin, labeled.
|
|
9
|
+
*
|
|
10
|
+
* This replaces waldemort's deploy-time monkey patch
|
|
11
|
+
* (patch-pilotswarm-dual-session-links.mjs), which anchor-matched the
|
|
12
|
+
* minified bundle and broke on every release. The plug-in surface is one
|
|
13
|
+
* env var on the portal server: PORTAL_LINK_ORIGINS.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Rebase one produced URL onto each configured origin.
|
|
18
|
+
* @param {string} url - a full URL produced against the current origin
|
|
19
|
+
* @param {Array<{label: string, origin: string}>} linkOrigins
|
|
20
|
+
* @returns {Array<{label: string|null, url: string}>} one entry per origin;
|
|
21
|
+
* with no (or one) configured origin, a single unlabeled entry with the
|
|
22
|
+
* original URL — zero behavior change for plain deployments.
|
|
23
|
+
*/
|
|
24
|
+
export function buildPortalLinks(url, linkOrigins) {
|
|
25
|
+
const origins = Array.isArray(linkOrigins) ? linkOrigins.filter((o) => o?.origin && o?.label) : [];
|
|
26
|
+
if (origins.length < 2) return [{ label: null, url: String(url || "") }];
|
|
27
|
+
let suffix;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(String(url || ""));
|
|
30
|
+
suffix = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
31
|
+
} catch {
|
|
32
|
+
return [{ label: null, url: String(url || "") }];
|
|
33
|
+
}
|
|
34
|
+
return origins.map((o) => ({ label: String(o.label), url: `${o.origin}${suffix}` }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clipboard shape: a bare URL for the single-origin case; one labeled line
|
|
39
|
+
* per origin otherwise, so the sender pastes both and the recipient picks
|
|
40
|
+
* the one their network can reach.
|
|
41
|
+
*/
|
|
42
|
+
export function formatPortalLinksForCopy(links) {
|
|
43
|
+
const list = Array.isArray(links) ? links : [];
|
|
44
|
+
if (list.length <= 1) return String(list[0]?.url || "");
|
|
45
|
+
return list.map((l) => `${l.label}: ${l.url}`).join("\n");
|
|
46
|
+
}
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -1570,6 +1570,39 @@ export function appReducer(state, action) {
|
|
|
1570
1570
|
if (previousActiveId && previousActiveId !== action.sessionId) {
|
|
1571
1571
|
savedChatScroll[previousActiveId] = Number(state.ui.scroll?.chat) || 0;
|
|
1572
1572
|
}
|
|
1573
|
+
// Per-session prompt drafts: a half-written message belongs to the
|
|
1574
|
+
// session it was written in. Stash the outgoing draft (text +
|
|
1575
|
+
// staged attachments) and restore the incoming one, exactly like
|
|
1576
|
+
// the chat-scroll memory above. Exception: while editing a pending
|
|
1577
|
+
// outbox message the composer text lives in that outbox item, not
|
|
1578
|
+
// in a draft — leave the item alone, drop the edit latch, and give
|
|
1579
|
+
// the incoming session its own draft back.
|
|
1580
|
+
const switchingSession = previousActiveId !== action.sessionId;
|
|
1581
|
+
const savedDrafts = { ...(state.ui.promptDraftBySession || {}) };
|
|
1582
|
+
let nextPrompt = state.ui.prompt;
|
|
1583
|
+
let nextPromptCursor = state.ui.promptCursor;
|
|
1584
|
+
let nextPromptAttachments = state.ui.promptAttachments;
|
|
1585
|
+
let nextPromptEdit = state.ui.promptEdit ?? null;
|
|
1586
|
+
if (switchingSession) {
|
|
1587
|
+
const editingPending = Boolean(state.ui.promptEdit);
|
|
1588
|
+
if (!editingPending && previousActiveId) {
|
|
1589
|
+
const outgoing = {
|
|
1590
|
+
prompt: String(state.ui.prompt || ""),
|
|
1591
|
+
attachments: Array.isArray(state.ui.promptAttachments) ? state.ui.promptAttachments : [],
|
|
1592
|
+
};
|
|
1593
|
+
if (outgoing.prompt || outgoing.attachments.length > 0) {
|
|
1594
|
+
savedDrafts[previousActiveId] = outgoing;
|
|
1595
|
+
} else {
|
|
1596
|
+
delete savedDrafts[previousActiveId];
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const incoming = action.sessionId ? savedDrafts[action.sessionId] : null;
|
|
1600
|
+
delete savedDrafts[action.sessionId];
|
|
1601
|
+
nextPrompt = incoming?.prompt || "";
|
|
1602
|
+
nextPromptCursor = nextPrompt.length;
|
|
1603
|
+
nextPromptAttachments = Array.isArray(incoming?.attachments) ? incoming.attachments : [];
|
|
1604
|
+
nextPromptEdit = null;
|
|
1605
|
+
}
|
|
1573
1606
|
// Manual navigation to a different session releases the deep-link
|
|
1574
1607
|
// latch and its transient filter exception.
|
|
1575
1608
|
const releasesNavigationLatch = Boolean(
|
|
@@ -1588,6 +1621,12 @@ export function appReducer(state, action) {
|
|
|
1588
1621
|
ui: {
|
|
1589
1622
|
...state.ui,
|
|
1590
1623
|
chatScrollBySession: savedChatScroll,
|
|
1624
|
+
promptDraftBySession: savedDrafts,
|
|
1625
|
+
prompt: nextPrompt,
|
|
1626
|
+
promptCursor: nextPromptCursor,
|
|
1627
|
+
promptRows: getPromptInputRows(nextPrompt),
|
|
1628
|
+
promptAttachments: nextPromptAttachments,
|
|
1629
|
+
promptEdit: nextPromptEdit,
|
|
1591
1630
|
// Status notices are per-moment, usually per-session (send
|
|
1592
1631
|
// refusals, access hints) — a stale one must not follow the
|
|
1593
1632
|
// user to the next session.
|
|
@@ -1861,6 +1900,8 @@ export function appReducer(state, action) {
|
|
|
1861
1900
|
for (const id of ids) delete nextOutbox[id];
|
|
1862
1901
|
const nextChatScroll = { ...(state.ui.chatScrollBySession || {}) };
|
|
1863
1902
|
for (const id of ids) delete nextChatScroll[id];
|
|
1903
|
+
const nextDrafts = { ...(state.ui.promptDraftBySession || {}) };
|
|
1904
|
+
for (const id of ids) delete nextDrafts[id];
|
|
1864
1905
|
return {
|
|
1865
1906
|
...state,
|
|
1866
1907
|
history: {
|
|
@@ -1874,6 +1915,7 @@ export function appReducer(state, action) {
|
|
|
1874
1915
|
ui: {
|
|
1875
1916
|
...state.ui,
|
|
1876
1917
|
chatScrollBySession: nextChatScroll,
|
|
1918
|
+
promptDraftBySession: nextDrafts,
|
|
1877
1919
|
},
|
|
1878
1920
|
};
|
|
1879
1921
|
}
|
|
@@ -2953,7 +2995,20 @@ export function appReducer(state, action) {
|
|
|
2953
2995
|
const dataRev = Number(action.dataRev);
|
|
2954
2996
|
if (!sessionId || !Number.isFinite(dataRev) || dataRev <= 0) return state;
|
|
2955
2997
|
const existing = state.canvas.bySessionId[key] || {};
|
|
2956
|
-
|
|
2998
|
+
const planeSeq = Number(action.planeSeq);
|
|
2999
|
+
const isPlane = Number.isFinite(planeSeq) && planeSeq > 0;
|
|
3000
|
+
// Plane takeover: once a slot has seen ONE plane-fed tick, that
|
|
3001
|
+
// lineage owns the slot. Plane ticks order by planeSeq (their
|
|
3002
|
+
// numbering restarts relative to legacy dataRev, so the numeric
|
|
3003
|
+
// guard below would wrongly starve them); legacy dual-written
|
|
3004
|
+
// events for a plane-owned slot are strictly staler copies of
|
|
3005
|
+
// state the plane already delivered — drop them.
|
|
3006
|
+
if (isPlane) {
|
|
3007
|
+
if ((existing.planeSeq || 0) >= planeSeq) return state;
|
|
3008
|
+
} else {
|
|
3009
|
+
if (existing.planeSeq) return state;
|
|
3010
|
+
if ((existing.latestDataRev || 0) >= dataRev) return state;
|
|
3011
|
+
}
|
|
2957
3012
|
return {
|
|
2958
3013
|
...state,
|
|
2959
3014
|
canvas: {
|
|
@@ -2962,14 +3017,45 @@ export function appReducer(state, action) {
|
|
|
2962
3017
|
...state.canvas.bySessionId,
|
|
2963
3018
|
[key]: {
|
|
2964
3019
|
...existing,
|
|
2965
|
-
|
|
3020
|
+
// The DISPLAYED tick number stays monotonic across
|
|
3021
|
+
// the takeover: viewed-marking and badges compare
|
|
3022
|
+
// against it, and a smaller number would replay
|
|
3023
|
+
// as "already seen".
|
|
3024
|
+
latestDataRev: Math.max(dataRev, existing.latestDataRev || 0),
|
|
3025
|
+
...(isPlane ? { planeSeq } : {}),
|
|
2966
3026
|
dataPayload: action.payload && typeof action.payload === "object" ? action.payload : null,
|
|
3027
|
+
// The patch that produced this state, for pages
|
|
3028
|
+
// that opt into targeted updates. Whole-state
|
|
3029
|
+
// ticks clear it — there is no delta to hand out.
|
|
3030
|
+
dataPatch: isPlane && action.patch && typeof action.patch === "object" ? action.patch : null,
|
|
2967
3031
|
},
|
|
2968
3032
|
},
|
|
2969
3033
|
},
|
|
2970
3034
|
};
|
|
2971
3035
|
}
|
|
2972
3036
|
|
|
3037
|
+
case "canvas/planeReleased": {
|
|
3038
|
+
// The plane died for this session: release the takeover on every
|
|
3039
|
+
// slot so legacy durable ticks resume applying. Displayed tick
|
|
3040
|
+
// numbers (latestDataRev) survive, so stale legacy dupes still
|
|
3041
|
+
// drop by ordinary ordering while fresh ones flow.
|
|
3042
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
3043
|
+
if (!sessionId) return state;
|
|
3044
|
+
let changed = false;
|
|
3045
|
+
const nextBySession = { ...state.canvas.bySessionId };
|
|
3046
|
+
for (let slot = 1; slot <= 5; slot++) {
|
|
3047
|
+
const key = canvasKey(sessionId, slot);
|
|
3048
|
+
const entry = nextBySession[key];
|
|
3049
|
+
if (entry && entry.planeSeq) {
|
|
3050
|
+
const { planeSeq: _released, ...rest } = entry;
|
|
3051
|
+
nextBySession[key] = rest;
|
|
3052
|
+
changed = true;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
if (!changed) return state;
|
|
3056
|
+
return { ...state, canvas: { ...state.canvas, bySessionId: nextBySession } };
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2973
3059
|
case "canvas/snapshot": {
|
|
2974
3060
|
const sessionId = String(action.sessionId || "").trim();
|
|
2975
3061
|
if (!sessionId) return state;
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -2769,6 +2769,8 @@ export function selectCanvasView(state) {
|
|
|
2769
2769
|
latestDataRev,
|
|
2770
2770
|
lastViewedDataRev,
|
|
2771
2771
|
dataPayload: entry?.dataPayload || null,
|
|
2772
|
+
dataPatch: entry?.dataPatch || null,
|
|
2773
|
+
planeSeq: entry?.planeSeq || 0,
|
|
2772
2774
|
unseen: exists && (latestRev > lastViewedRev || latestDataRev > lastViewedDataRev) && mode !== "canvas",
|
|
2773
2775
|
};
|
|
2774
2776
|
}
|