pilotswarm 0.5.35 → 0.5.37
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/app.js +0 -4
- package/tui/src/index.js +0 -9
- package/ui/core/src/canvas-actions.js +148 -0
- package/ui/core/src/commands.js +0 -1
- package/ui/core/src/controller.js +217 -19
- package/ui/core/src/history.js +134 -1
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/reducer.js +209 -19
- package/ui/core/src/selectors.js +143 -154
- package/ui/core/src/state.js +51 -2
- package/ui/core/src/themes/duroxide.js +6 -1
- package/ui/core/src/themes/win95.js +4 -1
- package/ui/react/src/components.js +1 -7
- package/ui/react/src/web-app.js +853 -260
- package/web/dist/assets/{abnfDiagram-VRR7QNED-BwJEohLG.js → abnfDiagram-VRR7QNED-DlvPH-8O.js} +1 -1
- package/web/dist/assets/{arc-CWUJ_S5s.js → arc-r1liz5e5.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-i6mrLxuE.js → architectureDiagram-ZJ3FMSHR-Bci5dNH3.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C_KG-qPQ.js → blockDiagram-677ZJIJ3-Bug32AXp.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-BBJH7hbB.js → c4Diagram-LMCZKHZV-DwxSFqgE.js} +1 -1
- package/web/dist/assets/channel-jVpSYat3.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CXwaFcaM.js → chunk-2Q5K7J3B-B_dbI_xg.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C8ujjkK-.js → chunk-32BRIVSS-BkF68W-R.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-M9rz7YYk.js → chunk-5VM5RSS4-BSbNIbJu.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-bk6tRxOA.js → chunk-EX3LRPZG-Blv_Iqa2.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-DZ4WSHGH.js → chunk-JWPE2WC7-Dk_dBFKd.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-B5-y6tMF.js → chunk-MOJQB5TN-DqNFBrfw.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-yKcnHxm0.js → chunk-RYQCIY6F-Dlv7Svpb.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-CpoNcATD.js → chunk-V7JOEXUC-DozGikwu.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-Bf1yD2fn.js → chunk-VR4S4FIN-CmkqvGT4.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-EnPvDVhP.js → chunk-XXDRQBXY-DyByai5N.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C78zSajE.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C78zSajE.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-qhKoA2qj.js → cose-bilkent-JH36ORCC-Bv-gRVvA.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-jcyYelJS.js → cynefin-VYW2F7L2-DRnAsyZU.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-D4RLufLP.js → cynefinDiagram-TSTJHNR4-B1fnUtjo.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-C9BA3GTO.js → dagre-VKFMJZFB-DM6-QENB.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DbSSrVUJ.js → diagram-FQU43EPY-BPxxnLdU.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-3egISQXQ.js → diagram-G47NLZAW-BgtJT3qF.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-8IKEMGRg.js → diagram-NH7WQ7WH-uekeIzsy.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-VnaogqtK.js → diagram-OA4YK3LP-BVy3JZWE.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-DxigXH0T.js → diagram-WEI45ONY-BWdjUr32.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-BSkNwJ9x.js → ebnfDiagram-CCIWWBDH-DD4fwhNK.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-CKN8JXl6.js → erDiagram-Q63AITRT-BllD_6Kh.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-DDaD5ExP.js → flowDiagram-23GEKE2U-QEYKkAUN.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-tOrBOuoI.js → ganttDiagram-NO4QXBWP-uZcfKTSW.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-Cdp867ON.js → gitGraphDiagram-IHSO6WYX-XPdbPaH-.js} +1 -1
- package/web/dist/assets/index-BuDSoRIL.css +1 -0
- package/web/dist/assets/{index-BmwU43_u.js → index-D1itTxvB.js} +4 -4
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DwcKHD2h.js → infoDiagram-FWYZ7A6U-DXCnQOJs.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CWffK5kz.js → ishikawaDiagram-FXEZZL3T-687_4FBy.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-BEAgS-EZ.js → journeyDiagram-5HDEW3XC-GXlKuda8.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-xDm-mRRj.js → kanban-definition-HUTT4EX6-C38-2DvN.js} +1 -1
- package/web/dist/assets/{linear-oP9MA0as.js → linear-lGPXiBcz.js} +1 -1
- package/web/dist/assets/{mermaid.core-DgXXt1OS.js → mermaid.core-C_neEtab.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BBQgVrYC.js → mindmap-definition-LN4V7U3C-BaJ9tpEG.js} +1 -1
- package/web/dist/assets/{papaparse.min-CR6JmBWm.js → papaparse.min-rDwKYbBf.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-C2O0cf3j.js → pegDiagram-2B236MQR-BdXJLYgY.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-D08pUtYU.js → pieDiagram-ENE6RG2P-DU19t6fM.js} +1 -1
- package/web/dist/assets/pilotswarm-BNKGG936.js +96 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-D-Z7YoEK.js → quadrantDiagram-ABIIQ3AL-CBkOdPp0.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-ClkdveLI.js → railroadDiagram-RFXS5EU6-D2DMcGDw.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-IY4xbn-Q.js → requirementDiagram-TGXJPOKE-Bx9tF1pr.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Cpt5i7KM.js → sankeyDiagram-HTMAVEWB-DcLlRfsi.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-c07lALcz.js → sequenceDiagram-DBY2YBRQ-xr6UVRBO.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CmCDiNbI.js → sizeCapture-X5ZJPWSS-0fUkSXmg.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-Dueu17zX.js → stateDiagram-2N3HPSRC-BWgJGGpg.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DmAC8DdB.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-Csc2Fup8.js → swimlanes-5IMT3BWC-CZKya4_i.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CVYx20_X.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-4-lf702e.js → timeline-definition-FHXFAJF6-Df8BpeAu.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-CmlaKpLI.js → vennDiagram-L72KCM5P-ChX5bjT6.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-B9Qe44pi.js → wardleyDiagram-EHGQE667-Dr48BgaJ.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-BFcc0CLn.js → xychartDiagram-FW5EYKEG-Cia_MFfc.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/runtime.js +20 -1
- package/web/dist/assets/channel-5Ddqi9ib.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B8u1fZiB.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B8u1fZiB.js +0 -1
- package/web/dist/assets/index-D-nA7gUe.css +0 -1
- package/web/dist/assets/pilotswarm-D7NLCbuc.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-AGpIV7wW.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTGZSV0N.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.37",
|
|
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.37",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
package/tui/src/app.js
CHANGED
|
@@ -932,10 +932,6 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
932
932
|
}
|
|
933
933
|
|
|
934
934
|
if (focus === "chat" || focus === "inspector" || focus === "activity") {
|
|
935
|
-
if (focus === "chat" && plainShortcut && input === "s") {
|
|
936
|
-
controller.handleCommand(UI_COMMANDS.TOGGLE_CHAT_VIEW).catch(() => {});
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
935
|
if (key.upArrow || input === "k") {
|
|
940
936
|
controller.handleCommand(UI_COMMANDS.SCROLL_UP).catch(() => {});
|
|
941
937
|
return;
|
package/tui/src/index.js
CHANGED
|
@@ -158,7 +158,6 @@ export async function startTuiApp(config) {
|
|
|
158
158
|
mode: config.mode,
|
|
159
159
|
branding: config.branding,
|
|
160
160
|
themeId: userConfig.themeId,
|
|
161
|
-
chatViewMode: userConfig.chatViewMode,
|
|
162
161
|
sessionOwnerFilter: userConfig.sessionOwnerFilter,
|
|
163
162
|
layoutAdjustments: userConfig.layoutAdjustments,
|
|
164
163
|
pinnedSessionIds: userConfig.pinnedSessionIds,
|
|
@@ -255,7 +254,6 @@ export async function startTuiApp(config) {
|
|
|
255
254
|
|
|
256
255
|
// Persist theme changes to config file
|
|
257
256
|
let lastPersistedThemeId = store.getState().ui.themeId;
|
|
258
|
-
let lastPersistedChatViewMode = store.getState().ui.chatViewMode;
|
|
259
257
|
let lastPersistedSessionOwnerFilter = JSON.stringify(store.getState().sessions.ownerFilter || null);
|
|
260
258
|
let lastPersistedLayoutAdjustments = JSON.stringify({
|
|
261
259
|
paneAdjust: store.getState().ui.layout?.paneAdjust || 0,
|
|
@@ -268,7 +266,6 @@ export async function startTuiApp(config) {
|
|
|
268
266
|
store.subscribe(() => {
|
|
269
267
|
const state = store.getState();
|
|
270
268
|
const currentThemeId = state.ui.themeId;
|
|
271
|
-
const currentChatViewMode = state.ui.chatViewMode;
|
|
272
269
|
const currentSessionOwnerFilter = state.sessions.ownerFilter || null;
|
|
273
270
|
const currentSessionOwnerFilterJson = JSON.stringify(currentSessionOwnerFilter);
|
|
274
271
|
const currentLayoutAdjustments = {
|
|
@@ -291,12 +288,6 @@ export async function startTuiApp(config) {
|
|
|
291
288
|
changed = true;
|
|
292
289
|
}
|
|
293
290
|
|
|
294
|
-
if ((currentChatViewMode === "summary" || currentChatViewMode === "transcript") && currentChatViewMode !== lastPersistedChatViewMode) {
|
|
295
|
-
lastPersistedChatViewMode = currentChatViewMode;
|
|
296
|
-
patch.chatViewMode = currentChatViewMode;
|
|
297
|
-
changed = true;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
291
|
if (currentSessionOwnerFilterJson !== lastPersistedSessionOwnerFilter) {
|
|
301
292
|
lastPersistedSessionOwnerFilter = currentSessionOwnerFilterJson;
|
|
302
293
|
patch.sessionOwnerFilter = currentSessionOwnerFilter;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas response actions — the browser half of the interactive-canvas
|
|
3
|
+
* contract.
|
|
4
|
+
*
|
|
5
|
+
* The agent draws the canvas AND declares, per draw, exactly which structured
|
|
6
|
+
* responses that page may send back (`responseContract` on `draw_canvas`,
|
|
7
|
+
* carried in `session.canvas_updated` event data). A page control posts
|
|
8
|
+
* `{type: "canvas-action", action, data}` to the parent; the CanvasFrame
|
|
9
|
+
* hands it here. This module is the whole decision:
|
|
10
|
+
*
|
|
11
|
+
* - NO contract on the current canvas → nothing is accepted. Interactivity
|
|
12
|
+
* is opt-in per revision, default-closed.
|
|
13
|
+
* - The action must be declared, every field must match its declared
|
|
14
|
+
* primitive type, no undeclared fields, hard size caps.
|
|
15
|
+
*
|
|
16
|
+
* A conforming action is formatted as a canonical `[canvas-action] {...}`
|
|
17
|
+
* user message. The transcript records it (provenance: it is a real message
|
|
18
|
+
* from the authenticated viewer), the agent reads the JSON, and the portal
|
|
19
|
+
* chat HIDES it — the canvas redraw is the user-visible half of the loop.
|
|
20
|
+
*
|
|
21
|
+
* Provenance (that the message came from the canvas iframe and not some other
|
|
22
|
+
* artifact preview) is enforced by the caller: the CanvasFrame accepts
|
|
23
|
+
* postMessage events only from its own live iframe's contentWindow. This
|
|
24
|
+
* module assumes that check already happened.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const CANVAS_ACTION_PREFIX = "[canvas-action] ";
|
|
28
|
+
|
|
29
|
+
const ACTION_MAX_STRING_CHARS = 2048;
|
|
30
|
+
const ACTION_MAX_SERIALIZED_BYTES = 8192;
|
|
31
|
+
|
|
32
|
+
/** Serialized-size guard that tolerates hosts without TextEncoder. */
|
|
33
|
+
function serializedByteLength(value) {
|
|
34
|
+
// Cyclic objects and BigInts cross postMessage (structured clone) but
|
|
35
|
+
// make JSON.stringify THROW — and this module's contract is that garbage
|
|
36
|
+
// is refused, never thrown, because the throw would escape the sync
|
|
37
|
+
// submit path as an uncaught error. Infinity fails the size check.
|
|
38
|
+
let json;
|
|
39
|
+
try {
|
|
40
|
+
json = JSON.stringify(value) || "";
|
|
41
|
+
} catch {
|
|
42
|
+
return Infinity;
|
|
43
|
+
}
|
|
44
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(json).length;
|
|
45
|
+
return json.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate one posted message against the current canvas contract.
|
|
50
|
+
* Returns `{ok: true, action, data}` or `{ok: false, reason}`; never throws
|
|
51
|
+
* on garbage — a hostile page must not be able to crash the portal shell.
|
|
52
|
+
*/
|
|
53
|
+
export function validateCanvasAction(contract, message) {
|
|
54
|
+
const actions = contract?.actions;
|
|
55
|
+
if (!actions || typeof actions !== "object") {
|
|
56
|
+
return { ok: false, reason: "the current canvas declares no response contract" };
|
|
57
|
+
}
|
|
58
|
+
if (!message || typeof message !== "object" || message.type !== "canvas-action") {
|
|
59
|
+
return { ok: false, reason: "not a canvas-action message" };
|
|
60
|
+
}
|
|
61
|
+
const action = typeof message.action === "string" ? message.action : "";
|
|
62
|
+
const declaredFields = Object.prototype.hasOwnProperty.call(actions, action) ? actions[action] : null;
|
|
63
|
+
if (!declaredFields || typeof declaredFields !== "object") {
|
|
64
|
+
return { ok: false, reason: `action ${JSON.stringify(action)} is not declared by the contract` };
|
|
65
|
+
}
|
|
66
|
+
const data = message.data === undefined ? {} : message.data;
|
|
67
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
68
|
+
return { ok: false, reason: "data must be an object" };
|
|
69
|
+
}
|
|
70
|
+
for (const key of Object.keys(data)) {
|
|
71
|
+
if (!Object.prototype.hasOwnProperty.call(declaredFields, key)) {
|
|
72
|
+
return { ok: false, reason: `field ${JSON.stringify(key)} is not declared for action ${JSON.stringify(action)}` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const clean = {};
|
|
76
|
+
for (const [field, declaredRaw] of Object.entries(declaredFields)) {
|
|
77
|
+
const declared = String(declaredRaw || "");
|
|
78
|
+
const optional = declared.endsWith("?");
|
|
79
|
+
const type = optional ? declared.slice(0, -1) : declared;
|
|
80
|
+
const value = data[field];
|
|
81
|
+
if (value === undefined || value === null) {
|
|
82
|
+
if (optional) continue;
|
|
83
|
+
return { ok: false, reason: `required field ${JSON.stringify(field)} is missing` };
|
|
84
|
+
}
|
|
85
|
+
if (type === "json") {
|
|
86
|
+
// One structured value for batched forms — a 23-item sign-off
|
|
87
|
+
// submits as a single object, not 23 round trips. Internal shape
|
|
88
|
+
// is the agent's contract with itself (it authored both sides);
|
|
89
|
+
// the serialized-size cap below is the only bound that matters.
|
|
90
|
+
if (typeof value !== "object") {
|
|
91
|
+
return { ok: false, reason: `field ${JSON.stringify(field)} must be a JSON object or array` };
|
|
92
|
+
}
|
|
93
|
+
clean[field] = value;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value !== type) {
|
|
97
|
+
return { ok: false, reason: `field ${JSON.stringify(field)} must be a ${type}` };
|
|
98
|
+
}
|
|
99
|
+
if (type === "string" && value.length > ACTION_MAX_STRING_CHARS) {
|
|
100
|
+
return { ok: false, reason: `field ${JSON.stringify(field)} exceeds ${ACTION_MAX_STRING_CHARS} characters` };
|
|
101
|
+
}
|
|
102
|
+
if (type === "number" && !Number.isFinite(value)) {
|
|
103
|
+
return { ok: false, reason: `field ${JSON.stringify(field)} must be a finite number` };
|
|
104
|
+
}
|
|
105
|
+
clean[field] = value;
|
|
106
|
+
}
|
|
107
|
+
if (serializedByteLength(clean) > ACTION_MAX_SERIALIZED_BYTES) {
|
|
108
|
+
return { ok: false, reason: `action payload exceeds ${ACTION_MAX_SERIALIZED_BYTES} bytes` };
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, action, data: clean };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The canonical wire text — what the agent reads and history flags on. */
|
|
114
|
+
export function formatCanvasActionPrompt(action, data) {
|
|
115
|
+
return `${CANVAS_ACTION_PREFIX}${JSON.stringify({ action, data })}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Detector for the history pipelines: is this user message a canvas action? */
|
|
119
|
+
export function isCanvasActionContent(content) {
|
|
120
|
+
return typeof content === "string" && content.startsWith(CANVAS_ACTION_PREFIX);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Parse the canonical text back to {action, data}; null when malformed. */
|
|
124
|
+
export function parseCanvasActionContent(content) {
|
|
125
|
+
if (!isCanvasActionContent(content)) return null;
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(content.slice(CANVAS_ACTION_PREFIX.length));
|
|
128
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.action !== "string") return null;
|
|
129
|
+
return { action: parsed.action, data: parsed.data && typeof parsed.data === "object" ? parsed.data : {} };
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A small sliding-window rate limiter: at most `burst` accepted actions per
|
|
137
|
+
* `windowMs`. The page is agent-authored JS speaking AS the viewer, so this
|
|
138
|
+
* is the bound on a buggy page auto-posting in a loop.
|
|
139
|
+
*/
|
|
140
|
+
export function createCanvasActionLimiter({ burst = 3, windowMs = 3000 } = {}) {
|
|
141
|
+
const stamps = [];
|
|
142
|
+
return function tryAcquire(now = Date.now()) {
|
|
143
|
+
while (stamps.length && now - stamps[0] > windowMs) stamps.shift();
|
|
144
|
+
if (stamps.length >= burst) return false;
|
|
145
|
+
stamps.push(now);
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
}
|
package/ui/core/src/commands.js
CHANGED
|
@@ -21,7 +21,6 @@ export const UI_COMMANDS = {
|
|
|
21
21
|
OPEN_MOVE_TO_GROUP: "openMoveToGroup",
|
|
22
22
|
CREATE_SESSION_GROUP: "createSessionGroup",
|
|
23
23
|
OPEN_ARTIFACT_UPLOAD: "openArtifactUpload",
|
|
24
|
-
TOGGLE_CHAT_VIEW: "toggleChatView",
|
|
25
24
|
CLOSE_MODAL: "closeModal",
|
|
26
25
|
MODAL_PREV: "modalPrev",
|
|
27
26
|
MODAL_NEXT: "modalNext",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getNextHistoryEventLimit,
|
|
11
11
|
} from "./history.js";
|
|
12
12
|
import { applySessionUsageEvent, cloneContextUsageSnapshot } from "./context-usage.js";
|
|
13
|
+
import { validateCanvasAction, formatCanvasActionPrompt, createCanvasActionLimiter } from "./canvas-actions.js";
|
|
13
14
|
import { shouldKeepRecoverableTransportWarning } from "./session-errors.js";
|
|
14
15
|
import {
|
|
15
16
|
computeLegacyLayout,
|
|
@@ -2444,6 +2445,11 @@ export class PilotSwarmUiController {
|
|
|
2444
2445
|
if (!existingHistory?.events?.length) {
|
|
2445
2446
|
await this.ensureSessionHistory(selected, { force: true }).catch(() => {});
|
|
2446
2447
|
}
|
|
2448
|
+
// Boot-restore can hydrate the selection through THIS branch
|
|
2449
|
+
// instead of loadSession (whether the profile poll beats the first
|
|
2450
|
+
// sessions refresh is a race), so the canvas snapshot must be
|
|
2451
|
+
// armed here too. Memoized — refreshes after the first are free.
|
|
2452
|
+
this.ensureCanvasSnapshot(selected).catch(() => {});
|
|
2447
2453
|
if (this.activeSessionSubscriptionId !== selected) {
|
|
2448
2454
|
this.attachActiveSession(selected);
|
|
2449
2455
|
}
|
|
@@ -4163,6 +4169,13 @@ export class PilotSwarmUiController {
|
|
|
4163
4169
|
await this.syncSessionDetail(sessionId).catch(() => {});
|
|
4164
4170
|
this.attachActiveSession(sessionId);
|
|
4165
4171
|
this.ensureInspectorData().catch(() => {});
|
|
4172
|
+
// Canvas snapshot rides the selection burst. Invalidate-then-fetch on
|
|
4173
|
+
// every selection, deliberately: the live event stream only covers the
|
|
4174
|
+
// ATTACHED session, so a session's canvas knowledge goes stale the
|
|
4175
|
+
// moment you switch away — re-selection is the continuity break, and
|
|
4176
|
+
// one indexed limit-1 row is noise against the burst above.
|
|
4177
|
+
this.dispatch({ type: "canvas/snapshotInvalidate", sessionId });
|
|
4178
|
+
this.ensureCanvasSnapshot(sessionId).catch(() => {});
|
|
4166
4179
|
}
|
|
4167
4180
|
|
|
4168
4181
|
/**
|
|
@@ -4229,6 +4242,135 @@ export class PilotSwarmUiController {
|
|
|
4229
4242
|
return true;
|
|
4230
4243
|
}
|
|
4231
4244
|
|
|
4245
|
+
/**
|
|
4246
|
+
* Apply a live `session.canvas_updated` event: converge content always,
|
|
4247
|
+
* flip the right column only when it is unmistakably appropriate.
|
|
4248
|
+
*
|
|
4249
|
+
* Flip guards (the artifact-presented set, plus the user's own choices):
|
|
4250
|
+
* active session only, fresh only, not already in canvas mode, and never
|
|
4251
|
+
* after the user has manually toggled away this session — their explicit
|
|
4252
|
+
* choice downgrades flips to the unseen-changes badge.
|
|
4253
|
+
*/
|
|
4254
|
+
applyCanvasUpdate(sessionId, event) {
|
|
4255
|
+
const rev = Number(event?.data?.rev);
|
|
4256
|
+
if (!sessionId || !Number.isFinite(rev) || rev <= 0) return false;
|
|
4257
|
+
this.dispatch({
|
|
4258
|
+
type: "canvas/updated",
|
|
4259
|
+
sessionId,
|
|
4260
|
+
rev,
|
|
4261
|
+
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
4262
|
+
sizeBytes: event?.data?.sizeBytes,
|
|
4263
|
+
responseContract: event?.data?.responseContract,
|
|
4264
|
+
});
|
|
4265
|
+
|
|
4266
|
+
const state = this.getState();
|
|
4267
|
+
if (sessionId !== state.sessions.activeSessionId) return true;
|
|
4268
|
+
if (state.canvas.prefs[sessionId]?.optedOut) return true;
|
|
4269
|
+
const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
|
|
4270
|
+
if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
|
|
4271
|
+
return true;
|
|
4272
|
+
}
|
|
4273
|
+
// canvas/flip rather than a plain mode change: it ticks flipSeq even
|
|
4274
|
+
// when the mode is already "canvas", because a phone can be off its
|
|
4275
|
+
// canvas tab while the mode state still says canvas — the tick is
|
|
4276
|
+
// what brings the tab back. On desktop the repeat dispatch is a no-op.
|
|
4277
|
+
this.dispatch({ type: "canvas/flip", sessionId });
|
|
4278
|
+
return true;
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
/**
|
|
4282
|
+
* The cold-load snapshot: one indexed lookup against the full durable log,
|
|
4283
|
+
* fired per session as part of the selection burst and memoized. Its
|
|
4284
|
+
* result is definitive both ways — an event means a canvas exists at that
|
|
4285
|
+
* rev; nothing means none has ever been drawn. The client never
|
|
4286
|
+
* blind-loads canvas.html on a hunch.
|
|
4287
|
+
*/
|
|
4288
|
+
/**
|
|
4289
|
+
* A structured response posted by the canvas page, on its way to the
|
|
4290
|
+
* agent. The CanvasFrame already proved provenance (the message came from
|
|
4291
|
+
* the live canvas iframe and nothing else); this is the rest of the
|
|
4292
|
+
* pipeline: validate against the CURRENT revision's contract (no contract
|
|
4293
|
+
* → accept nothing), rate-limit (the page is agent-authored JS speaking
|
|
4294
|
+
* as the viewer), then send it as a real user message through the
|
|
4295
|
+
* ordinary transport path — no outbox, no optimistic chat bubble. The
|
|
4296
|
+
* history pipeline recognizes the canonical prefix and keeps it out of
|
|
4297
|
+
* the portal chat pane; the transcript still records it for provenance.
|
|
4298
|
+
*/
|
|
4299
|
+
submitCanvasAction(sessionId, message) {
|
|
4300
|
+
if (!sessionId) return Promise.resolve({ ok: false, reason: "no session" });
|
|
4301
|
+
// Creator-only, mirrored from the server's enforcement (the server is
|
|
4302
|
+
// the authority; this is the friendly refusal). The canvas mutates —
|
|
4303
|
+
// a shared viewer may be looking at a different revision than the one
|
|
4304
|
+
// the creator is conversing through, so their clicks must not speak.
|
|
4305
|
+
const me = this.getState().auth?.principal || null;
|
|
4306
|
+
const ownerRow = this.getState().sessions.byId[sessionId]?.owner || null;
|
|
4307
|
+
if (me && ownerRow && (String(ownerRow.provider || "") !== String(me.provider || "")
|
|
4308
|
+
|| String(ownerRow.subject || "") !== String(me.subject || ""))) {
|
|
4309
|
+
return Promise.resolve({ ok: false, reason: "only the session's creator can use canvas controls" });
|
|
4310
|
+
}
|
|
4311
|
+
const contract = this.getState().canvas.bySessionId[sessionId]?.responseContract || null;
|
|
4312
|
+
const verdict = validateCanvasAction(contract, message);
|
|
4313
|
+
if (!verdict.ok) return Promise.resolve(verdict);
|
|
4314
|
+
if (!this.canvasActionLimiters) this.canvasActionLimiters = new Map();
|
|
4315
|
+
let limiter = this.canvasActionLimiters.get(sessionId);
|
|
4316
|
+
if (!limiter) {
|
|
4317
|
+
limiter = createCanvasActionLimiter();
|
|
4318
|
+
this.canvasActionLimiters.set(sessionId, limiter);
|
|
4319
|
+
}
|
|
4320
|
+
if (!limiter()) return Promise.resolve({ ok: false, reason: "rate limited" });
|
|
4321
|
+
const prompt = formatCanvasActionPrompt(verdict.action, verdict.data);
|
|
4322
|
+
return this.transport.sendMessage(sessionId, prompt, { enqueueOnly: true })
|
|
4323
|
+
.then(() => {
|
|
4324
|
+
this.syncSessionEvents(sessionId).catch(() => {});
|
|
4325
|
+
return { ok: true, action: verdict.action };
|
|
4326
|
+
})
|
|
4327
|
+
.catch((error) => ({ ok: false, reason: error?.message || String(error) }));
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
async ensureCanvasSnapshot(sessionId) {
|
|
4331
|
+
if (!sessionId) return;
|
|
4332
|
+
if (this.getState().canvas.bySessionId[sessionId]?.snapshotLoaded) return;
|
|
4333
|
+
if (typeof this.transport.getSessionEventsBefore !== "function") {
|
|
4334
|
+
// A transport without the filtered query (older direct mode) still
|
|
4335
|
+
// converges via live events; record the attempt so we do not spin.
|
|
4336
|
+
this.dispatch({ type: "canvas/snapshot", sessionId, rev: 0 });
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
try {
|
|
4340
|
+
const rows = await this.transport.getSessionEventsBefore(
|
|
4341
|
+
sessionId, Number.MAX_SAFE_INTEGER, 1, ["session.canvas_updated"],
|
|
4342
|
+
);
|
|
4343
|
+
const top = Array.isArray(rows) ? rows[0] : null;
|
|
4344
|
+
this.dispatch({
|
|
4345
|
+
type: "canvas/snapshot",
|
|
4346
|
+
sessionId,
|
|
4347
|
+
rev: Number(top?.data?.rev) || 0,
|
|
4348
|
+
note: typeof top?.data?.note === "string" ? top.data.note : "",
|
|
4349
|
+
sizeBytes: top?.data?.sizeBytes,
|
|
4350
|
+
responseContract: top?.data?.responseContract,
|
|
4351
|
+
});
|
|
4352
|
+
// The latest data tick is the page's cold-load state — replay it
|
|
4353
|
+
// so a freshly loaded shell can reconstruct without the agent.
|
|
4354
|
+
if ((Number(top?.data?.rev) || 0) > 0) {
|
|
4355
|
+
const dataRows = await this.transport.getSessionEventsBefore(
|
|
4356
|
+
sessionId, Number.MAX_SAFE_INTEGER, 1, ["session.canvas_data"],
|
|
4357
|
+
).catch(() => []);
|
|
4358
|
+
const tick = Array.isArray(dataRows) ? dataRows[0] : null;
|
|
4359
|
+
if (tick?.data?.dataRev) {
|
|
4360
|
+
this.dispatch({
|
|
4361
|
+
type: "canvas/data",
|
|
4362
|
+
sessionId,
|
|
4363
|
+
dataRev: Number(tick.data.dataRev),
|
|
4364
|
+
payload: tick.data.payload,
|
|
4365
|
+
note: typeof tick.data.note === "string" ? tick.data.note : "",
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
} catch {
|
|
4370
|
+
// Leave snapshotLoaded unset — the next selection retries.
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4232
4374
|
mergeSessionEvent(sessionId, event) {
|
|
4233
4375
|
if (!sessionId || !event) return false;
|
|
4234
4376
|
const state = this.getState();
|
|
@@ -4279,6 +4421,23 @@ export class PilotSwarmUiController {
|
|
|
4279
4421
|
if (event.eventType === "session.artifact_presented") {
|
|
4280
4422
|
this.maybeRevealPresentedArtifact(sessionId, event);
|
|
4281
4423
|
}
|
|
4424
|
+
// Canvas: CONTENT convergence is ungated — a stale canvas_updated in a
|
|
4425
|
+
// reconnect replay must still advance latestRev and refresh a mounted
|
|
4426
|
+
// pane. Only the FLIP is guarded (below). This deliberately diverges
|
|
4427
|
+
// from artifact_presented, which drops stale events outright: a
|
|
4428
|
+
// presentation is an action, a canvas is state.
|
|
4429
|
+
if (event.eventType === "session.canvas_updated") {
|
|
4430
|
+
this.applyCanvasUpdate(sessionId, event);
|
|
4431
|
+
}
|
|
4432
|
+
if (event.eventType === "session.canvas_data") {
|
|
4433
|
+
this.dispatch({
|
|
4434
|
+
type: "canvas/data",
|
|
4435
|
+
sessionId,
|
|
4436
|
+
dataRev: Number(event?.data?.dataRev),
|
|
4437
|
+
payload: event?.data?.payload,
|
|
4438
|
+
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4282
4441
|
this.reconcileOutboxAgainstEvent(sessionId, event);
|
|
4283
4442
|
const derivedModel = extractSessionModelFromEvent(event);
|
|
4284
4443
|
const currentSession = this.getState().sessions.byId[sessionId] || { sessionId };
|
|
@@ -4453,6 +4612,15 @@ export class PilotSwarmUiController {
|
|
|
4453
4612
|
const eligible = this.getMovableGroupSessionSelection();
|
|
4454
4613
|
|
|
4455
4614
|
if (eligible.length === 0) {
|
|
4615
|
+
// With a GROUP active, the folder button means "this group":
|
|
4616
|
+
// rename it. (Creating from here always made a second group —
|
|
4617
|
+
// reported as a surprise on the phone.)
|
|
4618
|
+
const active = state.sessions.activeSessionId
|
|
4619
|
+
? state.sessions.byId[state.sessions.activeSessionId]
|
|
4620
|
+
: null;
|
|
4621
|
+
if (active?.isGroup && active.groupId) {
|
|
4622
|
+
return this.openRenameGroupModal(active);
|
|
4623
|
+
}
|
|
4456
4624
|
// Nothing selected is a legitimate intent: make an empty folder to
|
|
4457
4625
|
// drag sessions into, rather than scolding the user.
|
|
4458
4626
|
return this.openCreateEmptyGroupModal();
|
|
@@ -4521,6 +4689,32 @@ export class PilotSwarmUiController {
|
|
|
4521
4689
|
return this.openMoveToGroupModal();
|
|
4522
4690
|
}
|
|
4523
4691
|
|
|
4692
|
+
/** Rename the active group in place (same modal, rename mode). */
|
|
4693
|
+
openRenameGroupModal(group) {
|
|
4694
|
+
if (typeof this.transport.updateSessionGroup !== "function") {
|
|
4695
|
+
this.dispatch({ type: "ui/status", text: "Group rename is not supported by this transport" });
|
|
4696
|
+
return null;
|
|
4697
|
+
}
|
|
4698
|
+
const state = this.getState();
|
|
4699
|
+
const current = String(group.title || "").trim();
|
|
4700
|
+
this.dispatch({
|
|
4701
|
+
type: "ui/modal",
|
|
4702
|
+
modal: {
|
|
4703
|
+
type: "sessionGroupName",
|
|
4704
|
+
mode: "rename",
|
|
4705
|
+
groupId: group.groupId,
|
|
4706
|
+
title: "Rename Group",
|
|
4707
|
+
previousFocus: state.ui.focusRegion,
|
|
4708
|
+
sessionIds: [],
|
|
4709
|
+
value: current,
|
|
4710
|
+
cursorIndex: current.length,
|
|
4711
|
+
maxLength: 80,
|
|
4712
|
+
},
|
|
4713
|
+
});
|
|
4714
|
+
this.dispatch({ type: "ui/status", text: "Edit the group name and press Enter" });
|
|
4715
|
+
return null;
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4524
4718
|
/** Name-and-create an EMPTY group (no sessions), ready to drag into. */
|
|
4525
4719
|
async openCreateEmptyGroupModal() {
|
|
4526
4720
|
if (typeof this.transport.createSessionGroup !== "function") {
|
|
@@ -4672,6 +4866,19 @@ export class PilotSwarmUiController {
|
|
|
4672
4866
|
this.dispatch({ type: "ui/status", text: "Group name cannot be empty" });
|
|
4673
4867
|
return;
|
|
4674
4868
|
}
|
|
4869
|
+
if (modal.mode === "rename" && modal.groupId) {
|
|
4870
|
+
const previousFocusRename = modal.previousFocus;
|
|
4871
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
4872
|
+
if (previousFocusRename) this.setFocus(previousFocusRename);
|
|
4873
|
+
try {
|
|
4874
|
+
await this.transport.updateSessionGroup(modal.groupId, { title });
|
|
4875
|
+
this.dispatch({ type: "ui/status", text: `Renamed group to "${title}"` });
|
|
4876
|
+
await this.refreshSessions().catch(() => {});
|
|
4877
|
+
} catch (error) {
|
|
4878
|
+
this.dispatch({ type: "ui/status", text: `Rename failed: ${error?.message || String(error)}` });
|
|
4879
|
+
}
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4675
4882
|
if (typeof this.transport.createSessionGroup !== "function") {
|
|
4676
4883
|
this.dispatch({ type: "ui/status", text: "Session grouping is not supported by this transport" });
|
|
4677
4884
|
return;
|
|
@@ -4728,22 +4935,7 @@ export class PilotSwarmUiController {
|
|
|
4728
4935
|
return sessionId ? state.sessions.byId[sessionId] || null : null;
|
|
4729
4936
|
}
|
|
4730
4937
|
|
|
4731
|
-
toggleChatViewMode() {
|
|
4732
|
-
const activeSession = this.getActiveSession();
|
|
4733
|
-
if (activeSession?.isGroup) {
|
|
4734
|
-
this.dispatch({ type: "ui/status", text: "Session groups show group details" });
|
|
4735
|
-
return;
|
|
4736
|
-
}
|
|
4737
|
-
const nextMode = this.getState().ui?.chatViewMode === "summary" ? "transcript" : "summary";
|
|
4738
|
-
this.dispatch({ type: "ui/chatViewMode", mode: nextMode });
|
|
4739
|
-
this.dispatch({ type: "ui/status", text: nextMode === "summary" ? "Showing session summary" : "Showing chat transcript" });
|
|
4740
|
-
}
|
|
4741
4938
|
|
|
4742
|
-
setChatViewMode(mode) {
|
|
4743
|
-
if (mode !== "summary" && mode !== "transcript") return;
|
|
4744
|
-
if (this.getActiveSession()?.isGroup) return;
|
|
4745
|
-
this.dispatch({ type: "ui/chatViewMode", mode });
|
|
4746
|
-
}
|
|
4747
4939
|
|
|
4748
4940
|
/**
|
|
4749
4941
|
* Opens the agent step of the new-session flow.
|
|
@@ -7829,7 +8021,9 @@ export class PilotSwarmUiController {
|
|
|
7829
8021
|
pane: "chat",
|
|
7830
8022
|
offset: 0,
|
|
7831
8023
|
});
|
|
7832
|
-
} else if (preserveChatView && previousScrollOffset > 0) {
|
|
8024
|
+
} else if (preserveChatView && previousScrollOffset > 0 && !options.autoTriggered) {
|
|
8025
|
+
// EXPLICIT "load older" (the TUI's `e`): jump up to the start
|
|
8026
|
+
// of what was just fetched — the user asked to SEE it.
|
|
7833
8027
|
const nextState = this.getState();
|
|
7834
8028
|
const nextRenderedLines = this.getActiveChatRenderMetrics(nextState).totalLines;
|
|
7835
8029
|
const addedLines = Math.max(0, nextRenderedLines - previousRenderedLines);
|
|
@@ -7841,6 +8035,13 @@ export class PilotSwarmUiController {
|
|
|
7841
8035
|
});
|
|
7842
8036
|
}
|
|
7843
8037
|
}
|
|
8038
|
+
// AUTO-triggered (scrolling reached the top): change NOTHING. The
|
|
8039
|
+
// offset is distance-from-BOTTOM, which prepended content cannot
|
|
8040
|
+
// move — leaving it alone keeps the viewport glued to the exact
|
|
8041
|
+
// messages the user was reading, and they scroll on into the new
|
|
8042
|
+
// page naturally. Adding the delta here teleported the view to
|
|
8043
|
+
// the TOP of a 3.3x-larger window — observed as "loading history
|
|
8044
|
+
// takes you way back instead of a few pages up".
|
|
7844
8045
|
|
|
7845
8046
|
const stateLabel = history.hasOlderEvents
|
|
7846
8047
|
? options.autoTriggered
|
|
@@ -8523,9 +8724,6 @@ export class PilotSwarmUiController {
|
|
|
8523
8724
|
case UI_COMMANDS.OPEN_ARTIFACT_UPLOAD:
|
|
8524
8725
|
this.openArtifactUploadModal();
|
|
8525
8726
|
return;
|
|
8526
|
-
case UI_COMMANDS.TOGGLE_CHAT_VIEW:
|
|
8527
|
-
this.toggleChatViewMode();
|
|
8528
|
-
return;
|
|
8529
8727
|
case UI_COMMANDS.CLOSE_MODAL:
|
|
8530
8728
|
this.closeModal();
|
|
8531
8729
|
return;
|