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/ui/core/src/history.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatHumanDurationSeconds, formatTimestamp, shortSessionId, stripTerminalMarkupTags, summarizeJson } from "./formatting.js";
|
|
2
|
+
import { isCanvasActionContent, parseCanvasActionContent } from "./canvas-actions.js";
|
|
2
3
|
import { formatCompactionActivityRuns } from "./context-usage.js";
|
|
3
4
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
4
5
|
|
|
@@ -30,6 +31,9 @@ export const CHAT_HISTORY_EVENT_TYPES = [
|
|
|
30
31
|
// silently never flipped. Observed live — two attempts died on
|
|
31
32
|
// ARTIFACT_TOO_LARGE with nothing shown in the transcript.
|
|
32
33
|
"session.regenerate_failed",
|
|
34
|
+
// Canvas revisions — kept in backward chat paging so the TUI's artifact
|
|
35
|
+
// link line survives history loads (the portal skips the line either way).
|
|
36
|
+
"session.canvas_updated",
|
|
33
37
|
];
|
|
34
38
|
|
|
35
39
|
// Build the inline transcript divider marking a session-regeneration epoch
|
|
@@ -379,6 +383,13 @@ function sharesClientMessageId(left, right) {
|
|
|
379
383
|
function areMessagesEquivalent(left, right) {
|
|
380
384
|
if (!left || !right) return false;
|
|
381
385
|
if (left.role !== right.role) return false;
|
|
386
|
+
// Canvas revision lines are platform-minted, exactly-once (one durable
|
|
387
|
+
// event per rev), and share identical text by design (the artifact://
|
|
388
|
+
// link) — the redelivery time-window below would eat rev N-1 on every
|
|
389
|
+
// reload, making the transcript disagree with what live append showed.
|
|
390
|
+
if (left.kind === "canvas-update" || right.kind === "canvas-update") {
|
|
391
|
+
return left.kind === right.kind && left.id === right.id;
|
|
392
|
+
}
|
|
382
393
|
|
|
383
394
|
const leftText = comparableMessageText(left);
|
|
384
395
|
const rightText = comparableMessageText(right);
|
|
@@ -458,6 +469,33 @@ function buildChatMessage(event, role) {
|
|
|
458
469
|
const sessionMessageCard = buildSessionMessageChatCard(event, rawText);
|
|
459
470
|
if (sessionMessageCard) return sessionMessageCard;
|
|
460
471
|
|
|
472
|
+
// A structured canvas response — the browser sent it on the viewer's
|
|
473
|
+
// behalf after validating it against the drawn contract. One item shape
|
|
474
|
+
// from ONE builder, so bulk load and live append cannot disagree. The
|
|
475
|
+
// portal chat hides it (the redraw is the visible half of the loop);
|
|
476
|
+
// the TUI shows a compact line.
|
|
477
|
+
if (role === "user" && isCanvasActionContent(rawText)) {
|
|
478
|
+
const parsed = parseCanvasActionContent(rawText);
|
|
479
|
+
if (parsed) {
|
|
480
|
+
return {
|
|
481
|
+
id: `${event.sessionId}:${event.seq}`,
|
|
482
|
+
kind: "canvas-action",
|
|
483
|
+
role: "user",
|
|
484
|
+
action: parsed.action,
|
|
485
|
+
data: parsed.data,
|
|
486
|
+
text: rawText,
|
|
487
|
+
time: formatTimestamp(event.createdAt),
|
|
488
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
489
|
+
// Without the ids, the distinct-ids rule can never fire and two
|
|
490
|
+
// genuine same-payload submissions within 10s collapse to one
|
|
491
|
+
// falsely-"redelivered" bubble while the activity feed shows two.
|
|
492
|
+
...(Array.isArray(event?.data?.clientMessageIds) && event.data.clientMessageIds.length > 0
|
|
493
|
+
? { clientMessageIds: event.data.clientMessageIds }
|
|
494
|
+
: {}),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
461
499
|
const text = stripLeadingSenderMarker(extractVisibleChatText(rawText, role));
|
|
462
500
|
if (!hasVisibleMessageText(text)) return null;
|
|
463
501
|
const clientMessageIds = Array.isArray(event?.data?.clientMessageIds)
|
|
@@ -711,6 +749,31 @@ function formatActivity(event) {
|
|
|
711
749
|
break;
|
|
712
750
|
}
|
|
713
751
|
|
|
752
|
+
case "session.canvas_updated": {
|
|
753
|
+
const data = (event?.data ?? {}) || {};
|
|
754
|
+
const rev = Number(data.rev) || 0;
|
|
755
|
+
const note = typeof data.note === "string" && data.note.trim() ? ` — ${data.note.trim()}` : "";
|
|
756
|
+
// Server-side artifact draws record their source; surfacing it here
|
|
757
|
+
// distinguishes "rendered the stored app" from an inline redraw the
|
|
758
|
+
// same honest way ticks are distinguished from draws.
|
|
759
|
+
const sourceName = typeof data.source?.filename === "string" && data.source.filename.trim()
|
|
760
|
+
? ` from ${data.source.filename.trim()}`
|
|
761
|
+
: "";
|
|
762
|
+
runs = buildLabeledActivityRuns(time, "[canvas]", "cyan", `rev ${rev}${sourceName}${note}`, "white");
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
case "session.canvas_data": {
|
|
767
|
+
// A data TICK, deliberately distinct from a full redraw: the page
|
|
768
|
+
// patched itself in place; no document was replaced.
|
|
769
|
+
const data = (event?.data ?? {}) || {};
|
|
770
|
+
const dataRev = Number(data.dataRev) || 0;
|
|
771
|
+
const kb = Number.isFinite(Number(data.sizeBytes)) ? ` (${(Number(data.sizeBytes) / 1024).toFixed(1)} KB)` : "";
|
|
772
|
+
const note = typeof data.note === "string" && data.note.trim() ? ` — ${data.note.trim()}` : "";
|
|
773
|
+
runs = buildLabeledActivityRuns(time, "[canvas]", "cyan", `data tick ${dataRev}${kb}${note}`, "white");
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
|
|
714
777
|
case "tool.execution_start":
|
|
715
778
|
runs = formatToolActivityRuns(time, event, "start");
|
|
716
779
|
break;
|
|
@@ -934,6 +997,43 @@ export function getNextHistoryEventLimit(currentLimit = DEFAULT_HISTORY_EVENT_LI
|
|
|
934
997
|
return nextLimit || Math.min(100_000, safeCurrent * 2);
|
|
935
998
|
}
|
|
936
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* The canvas event's chat item, shared verbatim by the bulk loader and the
|
|
1002
|
+
* live append path — a reload must not change the transcript. `text` carries
|
|
1003
|
+
* the bare artifact URI because the TUI's press-a picker extracts
|
|
1004
|
+
* `artifact://` links from message text; the rendered line builds its own
|
|
1005
|
+
* runs and never shows this string.
|
|
1006
|
+
*/
|
|
1007
|
+
function buildCanvasChatItem(event) {
|
|
1008
|
+
const sessionId = event.sessionId || null;
|
|
1009
|
+
return {
|
|
1010
|
+
id: `${event.sessionId || ""}:${event.seq}:canvas`,
|
|
1011
|
+
kind: "canvas-update",
|
|
1012
|
+
role: "canvas-update",
|
|
1013
|
+
sessionId,
|
|
1014
|
+
rev: Number(event?.data?.rev) || 0,
|
|
1015
|
+
note: typeof event?.data?.note === "string" ? event.data.note : "",
|
|
1016
|
+
text: sessionId ? `artifact://${sessionId}/canvas.html` : "",
|
|
1017
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** The diagnostic feed line for a validated canvas response. */
|
|
1022
|
+
function buildCanvasActionActivity(event, item) {
|
|
1023
|
+
const time = formatTimestamp(event.createdAt);
|
|
1024
|
+
const detail = `action ${item.action}${Object.keys(item.data || {}).length ? " " + summarizeJson(item.data) : ""}`;
|
|
1025
|
+
const runs = buildLabeledActivityRuns(time, "[canvas]", "cyan", detail, "white");
|
|
1026
|
+
return {
|
|
1027
|
+
id: `${event.sessionId}:${event.seq}:canvas-action`,
|
|
1028
|
+
eventType: event.eventType,
|
|
1029
|
+
time,
|
|
1030
|
+
seq: Number.isFinite(Number(event?.seq)) ? Number(event.seq) : 0,
|
|
1031
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
1032
|
+
text: flattenRunsText(runs),
|
|
1033
|
+
line: runs,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
937
1037
|
export function buildHistoryModel(events = [], options = {}) {
|
|
938
1038
|
const requestedLimit = Math.max(
|
|
939
1039
|
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
@@ -949,6 +1049,23 @@ export function buildHistoryModel(events = [], options = {}) {
|
|
|
949
1049
|
activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "user"));
|
|
950
1050
|
const message = buildChatMessage(event, "user");
|
|
951
1051
|
if (message) chat.push(message);
|
|
1052
|
+
if (message?.kind === "canvas-action") {
|
|
1053
|
+
const actionActivity = buildCanvasActionActivity(event, message);
|
|
1054
|
+
if (actionActivity) activity.push(actionActivity);
|
|
1055
|
+
}
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (event.eventType === "session.canvas_updated") {
|
|
1059
|
+
// A flagged chat item, not a plain message: the PORTAL chat skips
|
|
1060
|
+
// it entirely (the canvas pane updating is the signal there),
|
|
1061
|
+
// while the TUI — which cannot render the canvas — shows it as an
|
|
1062
|
+
// ordinary artifact link. Shared data, host-chosen affordance,
|
|
1063
|
+
// same precedent as the download hint. `text` carries the bare
|
|
1064
|
+
// artifact URI for the TUI's press-a picker, which extracts
|
|
1065
|
+
// `artifact://` links from message text.
|
|
1066
|
+
chat.push(buildCanvasChatItem(event));
|
|
1067
|
+
const activityItem = formatActivity(event);
|
|
1068
|
+
if (activityItem) activity.push(activityItem);
|
|
952
1069
|
continue;
|
|
953
1070
|
}
|
|
954
1071
|
if (event.eventType === "assistant.message") {
|
|
@@ -1038,8 +1155,12 @@ export function appendEventToHistory(history, event) {
|
|
|
1038
1155
|
|
|
1039
1156
|
if (event.eventType === "user.message") {
|
|
1040
1157
|
next.activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "user"));
|
|
1041
|
-
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
1042
1158
|
const message = buildChatMessage(event, "user");
|
|
1159
|
+
if (message?.kind === "canvas-action") {
|
|
1160
|
+
const actionActivity = buildCanvasActionActivity(event, message);
|
|
1161
|
+
if (actionActivity) next.activity.push(actionActivity);
|
|
1162
|
+
}
|
|
1163
|
+
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
1043
1164
|
if (!message) return next;
|
|
1044
1165
|
// Prospective flag: covers a bulk load where the stop event arrived
|
|
1045
1166
|
// before this message in the reduce order.
|
|
@@ -1052,6 +1173,18 @@ export function appendEventToHistory(history, event) {
|
|
|
1052
1173
|
next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
|
|
1053
1174
|
return next;
|
|
1054
1175
|
}
|
|
1176
|
+
if (event.eventType === "session.canvas_updated") {
|
|
1177
|
+
// Same flagged chat item the bulk loader builds — the live path and
|
|
1178
|
+
// the history path must agree or a reload would change the transcript.
|
|
1179
|
+
next.chat.push(buildCanvasChatItem(event));
|
|
1180
|
+
next.chat = clampHistoryItems(next.chat, loadedEventLimit);
|
|
1181
|
+
const canvasActivity = formatActivity(event);
|
|
1182
|
+
if (canvasActivity) {
|
|
1183
|
+
next.activity.push(canvasActivity);
|
|
1184
|
+
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
1185
|
+
}
|
|
1186
|
+
return next;
|
|
1187
|
+
}
|
|
1055
1188
|
if (event.eventType === "assistant.message") {
|
|
1056
1189
|
next.activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "assistant"));
|
|
1057
1190
|
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
package/ui/core/src/index.js
CHANGED
|
@@ -13,3 +13,4 @@ export * from "./selectors.js";
|
|
|
13
13
|
export * from "./controller.js";
|
|
14
14
|
export { isThemeLight } from "./themes/helpers.js";
|
|
15
15
|
export * from "./themes/index.js";
|
|
16
|
+
export { validateCanvasAction, formatCanvasActionPrompt, parseCanvasActionContent, isCanvasActionContent, createCanvasActionLimiter, CANVAS_ACTION_PREFIX } from "./canvas-actions.js";
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
normalizeArtifactEntries,
|
|
9
9
|
normalizeSessionOwnerFilter,
|
|
10
10
|
normalizeStoredActiveSessionId,
|
|
11
|
+
normalizeStoredCanvasPrefs,
|
|
11
12
|
normalizeStoredCollapsedSessionIds,
|
|
12
13
|
normalizeStoredLayoutAdjustments,
|
|
13
14
|
normalizeStoredPinnedSessionIds,
|
|
@@ -87,6 +88,39 @@ function prunePinnedIds(ids, byId) {
|
|
|
87
88
|
return out;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Fold a freshly applied profile's canvasPrefs over the local map. Two rules:
|
|
93
|
+
*
|
|
94
|
+
* - `lastViewedRev` merges by MAX. A poll snapshotted before a local view and
|
|
95
|
+
* applied after must not regress what this window has already seen — same
|
|
96
|
+
* monotonicity the revision counter itself keeps.
|
|
97
|
+
* - Entries prune like pins: absent-from-listing proves nothing (the listing
|
|
98
|
+
* is paged), but a session PRESENT as a child or group can never have a
|
|
99
|
+
* canvas, and a default-valued entry ({optedOut:false, lastViewedRev:0})
|
|
100
|
+
* says nothing worth storing. Both drop, which is what keeps the map from
|
|
101
|
+
* growing one dead key per session forever.
|
|
102
|
+
*/
|
|
103
|
+
function mergeCanvasPrefs(local, stored, byId) {
|
|
104
|
+
const out = {};
|
|
105
|
+
const keys = new Set([...Object.keys(stored || {}), ...Object.keys(local || {})]);
|
|
106
|
+
for (const sessionId of keys) {
|
|
107
|
+
const storedEntry = (stored || {})[sessionId];
|
|
108
|
+
const localEntry = (local || {})[sessionId];
|
|
109
|
+
const merged = {
|
|
110
|
+
// A local-only entry (viewed since the poll was snapshotted) must
|
|
111
|
+
// survive the apply — dropping it un-views what this window saw.
|
|
112
|
+
optedOut: storedEntry ? storedEntry.optedOut : Boolean(localEntry?.optedOut),
|
|
113
|
+
lastViewedRev: Math.max(storedEntry?.lastViewedRev || 0, localEntry?.lastViewedRev || 0),
|
|
114
|
+
lastViewedDataRev: Math.max(storedEntry?.lastViewedDataRev || 0, localEntry?.lastViewedDataRev || 0),
|
|
115
|
+
};
|
|
116
|
+
if (!merged.optedOut && merged.lastViewedRev === 0 && merged.lastViewedDataRev === 0) continue;
|
|
117
|
+
const session = byId?.[sessionId];
|
|
118
|
+
if (session && (session.parentSessionId || session.isGroup)) continue;
|
|
119
|
+
out[sessionId] = merged;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
|
|
91
125
|
/**
|
|
92
126
|
* Keep a collapse flag unless the session is genuinely gone.
|
|
@@ -772,8 +806,9 @@ export function appReducer(state, action) {
|
|
|
772
806
|
// say "rich" fall through to the transcript below.
|
|
773
807
|
const hasTouchScale = Object.prototype.hasOwnProperty.call(settings, "touchScale")
|
|
774
808
|
&& typeof settings.touchScale === "boolean";
|
|
775
|
-
const
|
|
776
|
-
&& (settings.
|
|
809
|
+
const hasRightPaneMode = Object.prototype.hasOwnProperty.call(settings, "rightPaneMode")
|
|
810
|
+
&& (settings.rightPaneMode === "canvas" || settings.rightPaneMode === "panes");
|
|
811
|
+
const hasCanvasPrefs = Object.prototype.hasOwnProperty.call(settings, "canvasPrefs");
|
|
777
812
|
const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
|
|
778
813
|
const hasCollapsed = Object.prototype.hasOwnProperty.call(settings, "collapsedSessionIds");
|
|
779
814
|
const hasSessionOrder = Object.prototype.hasOwnProperty.call(settings, "sessionOrder");
|
|
@@ -836,28 +871,15 @@ export function appReducer(state, action) {
|
|
|
836
871
|
...selection.ui,
|
|
837
872
|
themeId: hasTheme ? settings.themeId.trim() : selection.ui.themeId,
|
|
838
873
|
touchScale: hasTouchScale ? settings.touchScale : selection.ui.touchScale,
|
|
839
|
-
|
|
874
|
+
rightPaneMode: hasRightPaneMode ? settings.rightPaneMode : selection.ui.rightPaneMode,
|
|
840
875
|
layout: nextLayout,
|
|
841
876
|
},
|
|
877
|
+
canvas: hasCanvasPrefs
|
|
878
|
+
? { ...state.canvas, prefs: mergeCanvasPrefs(state.canvas.prefs, normalizeStoredCanvasPrefs(settings.canvasPrefs), hasLoadedSessions ? state.sessions.byId : null) }
|
|
879
|
+
: state.canvas,
|
|
842
880
|
};
|
|
843
881
|
}
|
|
844
882
|
|
|
845
|
-
case "ui/chatViewMode":
|
|
846
|
-
if (action.mode !== "summary" && action.mode !== "transcript") {
|
|
847
|
-
return state;
|
|
848
|
-
}
|
|
849
|
-
return {
|
|
850
|
-
...state,
|
|
851
|
-
ui: {
|
|
852
|
-
...state.ui,
|
|
853
|
-
chatViewMode: action.mode,
|
|
854
|
-
scroll: {
|
|
855
|
-
...state.ui.scroll,
|
|
856
|
-
chat: 0,
|
|
857
|
-
},
|
|
858
|
-
},
|
|
859
|
-
};
|
|
860
|
-
|
|
861
883
|
case "ui/modal":
|
|
862
884
|
return {
|
|
863
885
|
...state,
|
|
@@ -2587,6 +2609,174 @@ export function appReducer(state, action) {
|
|
|
2587
2609
|
},
|
|
2588
2610
|
};
|
|
2589
2611
|
|
|
2612
|
+
// ── Canvas ──────────────────────────────────────────────────────
|
|
2613
|
+
// The right column's mode plus the per-session attention state that
|
|
2614
|
+
// decides flip-vs-badge. Viewed-marking deliberately does NOT live in
|
|
2615
|
+
// these mode transitions: only the rendering surface knows what is
|
|
2616
|
+
// actually on screen, so `canvas/viewed` is dispatched by the canvas
|
|
2617
|
+
// pane when a revision really displays. Inferring "viewed" from mode
|
|
2618
|
+
// changes marked revs seen on a phone whose layout never showed the
|
|
2619
|
+
// pane, and left session-switches-while-in-canvas-mode unviewed.
|
|
2620
|
+
// - entering canvas mode is always a user gesture → clears opt-out
|
|
2621
|
+
// - a MANUAL switch away records the opt-out (future draws badge)
|
|
2622
|
+
// - `canvas/flip` is the agent-driven variant: same mode change,
|
|
2623
|
+
// plus a flipSeq tick that hosts with their own pane state (the
|
|
2624
|
+
// phone's tab strip) listen to
|
|
2625
|
+
case "ui/rightPaneMode": {
|
|
2626
|
+
const mode = action.mode === "canvas" ? "canvas" : "panes";
|
|
2627
|
+
const sessionId = action.sessionId || state.sessions.activeSessionId || null;
|
|
2628
|
+
let prefs = state.canvas.prefs;
|
|
2629
|
+
if (sessionId) {
|
|
2630
|
+
const prev = prefs[sessionId] || { optedOut: false, lastViewedRev: 0 };
|
|
2631
|
+
if (mode === "canvas" && prev.optedOut) {
|
|
2632
|
+
// Coming back by hand un-opts-out: the user has shown the
|
|
2633
|
+
// canvas is welcome again, so future draws may flip.
|
|
2634
|
+
prefs = { ...prefs, [sessionId]: { ...prev, optedOut: false } };
|
|
2635
|
+
} else if (mode === "panes" && action.manual && !prev.optedOut) {
|
|
2636
|
+
prefs = { ...prefs, [sessionId]: { ...prev, optedOut: true } };
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return {
|
|
2640
|
+
...state,
|
|
2641
|
+
ui: { ...state.ui, rightPaneMode: mode },
|
|
2642
|
+
canvas: { ...state.canvas, prefs },
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// The agent-driven flip. The guards (active session, freshness,
|
|
2647
|
+
// opt-out) live in the controller; by the time this dispatches the
|
|
2648
|
+
// flip is decided. flipSeq ticks even when the mode is already
|
|
2649
|
+
// "canvas" — a phone can be off the canvas tab while the persisted
|
|
2650
|
+
// mode still says canvas, and the tick is what brings its tab back.
|
|
2651
|
+
case "canvas/flip": {
|
|
2652
|
+
return {
|
|
2653
|
+
...state,
|
|
2654
|
+
ui: { ...state.ui, rightPaneMode: "canvas" },
|
|
2655
|
+
canvas: { ...state.canvas, flipSeq: (state.canvas.flipSeq || 0) + 1 },
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// The surface's receipt: revision `rev` (a draw) and/or `dataRev` (a
|
|
2660
|
+
// tick) actually rendered on screen. Both feed the unseen badge.
|
|
2661
|
+
case "canvas/viewed": {
|
|
2662
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
2663
|
+
if (!sessionId) return state;
|
|
2664
|
+
const rev = Number(action.rev);
|
|
2665
|
+
const dataRev = Number(action.dataRev);
|
|
2666
|
+
const prev = state.canvas.prefs[sessionId] || { optedOut: false, lastViewedRev: 0, lastViewedDataRev: 0 };
|
|
2667
|
+
const nextRev = Number.isFinite(rev) && rev > (prev.lastViewedRev || 0) ? rev : (prev.lastViewedRev || 0);
|
|
2668
|
+
const nextDataRev = Number.isFinite(dataRev) && dataRev > (prev.lastViewedDataRev || 0) ? dataRev : (prev.lastViewedDataRev || 0);
|
|
2669
|
+
if (nextRev === (prev.lastViewedRev || 0) && nextDataRev === (prev.lastViewedDataRev || 0)) return state;
|
|
2670
|
+
return {
|
|
2671
|
+
...state,
|
|
2672
|
+
canvas: {
|
|
2673
|
+
...state.canvas,
|
|
2674
|
+
prefs: { ...state.canvas.prefs, [sessionId]: { ...prev, lastViewedRev: nextRev, lastViewedDataRev: nextDataRev } },
|
|
2675
|
+
},
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
case "canvas/updated": {
|
|
2680
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
2681
|
+
const rev = Number(action.rev);
|
|
2682
|
+
if (!sessionId || !Number.isFinite(rev) || rev <= 0) return state;
|
|
2683
|
+
const existing = state.canvas.bySessionId[sessionId] || {};
|
|
2684
|
+
// Monotonic: replays and out-of-order merges must never regress
|
|
2685
|
+
// the known revision (freshness gates the FLIP, never the data).
|
|
2686
|
+
if ((existing.latestRev || 0) >= rev) return state;
|
|
2687
|
+
return {
|
|
2688
|
+
...state,
|
|
2689
|
+
canvas: {
|
|
2690
|
+
...state.canvas,
|
|
2691
|
+
bySessionId: {
|
|
2692
|
+
...state.canvas.bySessionId,
|
|
2693
|
+
[sessionId]: {
|
|
2694
|
+
...existing,
|
|
2695
|
+
latestRev: rev,
|
|
2696
|
+
note: typeof action.note === "string" ? action.note : "",
|
|
2697
|
+
sizeBytes: Number.isFinite(Number(action.sizeBytes)) ? Number(action.sizeBytes) : null,
|
|
2698
|
+
// The contract belongs to the revision: a draw
|
|
2699
|
+
// without one REVOKES interactivity (default
|
|
2700
|
+
// closed), so no carry-over from the previous rev.
|
|
2701
|
+
responseContract: action.responseContract && typeof action.responseContract === "object"
|
|
2702
|
+
? action.responseContract
|
|
2703
|
+
: null,
|
|
2704
|
+
},
|
|
2705
|
+
},
|
|
2706
|
+
},
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// A data tick: content for the page, in place. Never flips (ticks do
|
|
2711
|
+
// not interrupt) — but it DOES count toward the unseen badge via
|
|
2712
|
+
// latestDataRev vs lastViewedDataRev. Monotonic like the draw rev.
|
|
2713
|
+
case "canvas/data": {
|
|
2714
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
2715
|
+
const dataRev = Number(action.dataRev);
|
|
2716
|
+
if (!sessionId || !Number.isFinite(dataRev) || dataRev <= 0) return state;
|
|
2717
|
+
const existing = state.canvas.bySessionId[sessionId] || {};
|
|
2718
|
+
if ((existing.latestDataRev || 0) >= dataRev) return state;
|
|
2719
|
+
return {
|
|
2720
|
+
...state,
|
|
2721
|
+
canvas: {
|
|
2722
|
+
...state.canvas,
|
|
2723
|
+
bySessionId: {
|
|
2724
|
+
...state.canvas.bySessionId,
|
|
2725
|
+
[sessionId]: {
|
|
2726
|
+
...existing,
|
|
2727
|
+
latestDataRev: dataRev,
|
|
2728
|
+
dataPayload: action.payload && typeof action.payload === "object" ? action.payload : null,
|
|
2729
|
+
},
|
|
2730
|
+
},
|
|
2731
|
+
},
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
case "canvas/snapshot": {
|
|
2736
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
2737
|
+
if (!sessionId) return state;
|
|
2738
|
+
const existing = state.canvas.bySessionId[sessionId] || {};
|
|
2739
|
+
const rev = Number(action.rev) || 0;
|
|
2740
|
+
return {
|
|
2741
|
+
...state,
|
|
2742
|
+
canvas: {
|
|
2743
|
+
...state.canvas,
|
|
2744
|
+
bySessionId: {
|
|
2745
|
+
...state.canvas.bySessionId,
|
|
2746
|
+
[sessionId]: {
|
|
2747
|
+
...existing,
|
|
2748
|
+
// Events may have beaten the snapshot; never regress.
|
|
2749
|
+
latestRev: Math.max(existing.latestRev || 0, rev),
|
|
2750
|
+
note: (existing.latestRev || 0) > rev ? existing.note : (typeof action.note === "string" ? action.note : existing.note || ""),
|
|
2751
|
+
sizeBytes: (existing.latestRev || 0) > rev ? existing.sizeBytes : (Number.isFinite(Number(action.sizeBytes)) ? Number(action.sizeBytes) : existing.sizeBytes ?? null),
|
|
2752
|
+
responseContract: (existing.latestRev || 0) > rev
|
|
2753
|
+
? (existing.responseContract ?? null)
|
|
2754
|
+
: (action.responseContract && typeof action.responseContract === "object" ? action.responseContract : null),
|
|
2755
|
+
snapshotLoaded: true,
|
|
2756
|
+
},
|
|
2757
|
+
},
|
|
2758
|
+
},
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// Continuity break (windowed bulk reload replaced gap-free replay):
|
|
2763
|
+
// the memoized snapshot can no longer be trusted — re-take on next need.
|
|
2764
|
+
case "canvas/snapshotInvalidate": {
|
|
2765
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
2766
|
+
const existing = state.canvas.bySessionId[sessionId];
|
|
2767
|
+
if (!sessionId || !existing?.snapshotLoaded) return state;
|
|
2768
|
+
return {
|
|
2769
|
+
...state,
|
|
2770
|
+
canvas: {
|
|
2771
|
+
...state.canvas,
|
|
2772
|
+
bySessionId: {
|
|
2773
|
+
...state.canvas.bySessionId,
|
|
2774
|
+
[sessionId]: { ...existing, snapshotLoaded: false },
|
|
2775
|
+
},
|
|
2776
|
+
},
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2590
2780
|
case "files/htmlViewMode":
|
|
2591
2781
|
return {
|
|
2592
2782
|
...state,
|