pilotswarm 0.5.56 → 0.5.58
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 +5 -0
- package/ui/core/src/controller.js +117 -4
- package/ui/core/src/history.js +112 -1
- package/ui/core/src/reducer.js +177 -11
- package/ui/core/src/selectors.js +239 -18
- package/ui/core/src/state.js +65 -0
- package/ui/react/src/web-app.js +251 -27
- package/web/api/live-plane.js +193 -0
- package/web/api/ws.js +118 -2
- package/web/dist/assets/{abnfDiagram-VRR7QNED-Cccg5mUx.js → abnfDiagram-VRR7QNED-CCTGbkH5.js} +1 -1
- package/web/dist/assets/{arc-DFWp1OiI.js → arc-w8Hl2ORd.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-o2B4-sJ1.js → architectureDiagram-ZJ3FMSHR-DAFM73jL.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Cf_7Cm38.js → blockDiagram-677ZJIJ3-BYfTQFe7.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CKXuPB0d.js → c4Diagram-LMCZKHZV-bNF2gRsg.js} +1 -1
- package/web/dist/assets/channel-s4nVWOVf.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CZC3HwzC.js → chunk-2Q5K7J3B-DhqJnJ4W.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-7_X1k3eB.js → chunk-32BRIVSS-Jg1a6n_3.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-E2rxT4um.js → chunk-5VM5RSS4-y7NKpClZ.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BbW37HWx.js → chunk-EX3LRPZG-Lks3EhVc.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-dj0DOnPl.js → chunk-JWPE2WC7-BjcSkfk2.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-TeWalpvK.js → chunk-MOJQB5TN-USDZsR-1.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-ClHcH2bN.js → chunk-RYQCIY6F--HnsBTqS.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-Dy5XEnNL.js → chunk-V7JOEXUC-CUql3Oy3.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-Den_q-0P.js → chunk-VR4S4FIN-CpS-l3FG.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DZ1s-8xB.js → chunk-XXDRQBXY-Bjb7ifTV.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-tj8tRjww.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-tj8tRjww.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-CKAukUTf.js → cose-bilkent-JH36ORCC-CmY3-Xrd.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CM8y129F.js → cynefin-VYW2F7L2-DVtcyDi5.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-DLryvwYt.js → cynefinDiagram-TSTJHNR4-BPqkLecJ.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-BO0LMWmO.js → dagre-VKFMJZFB-B73fSRpn.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DiFLu7v2.js → diagram-FQU43EPY-uRCPZyuW.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DMkq9we0.js → diagram-G47NLZAW-CHs_l9pw.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-CxVQL8Dq.js → diagram-NH7WQ7WH-BDs35ueJ.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-B5TrTlUc.js → diagram-OA4YK3LP-Dsv48zWW.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Ca38mf1v.js → diagram-WEI45ONY-Bty-lxLj.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DZR_44LD.js → ebnfDiagram-CCIWWBDH-DgaxsuDg.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-Ca8mmsU1.js → erDiagram-Q63AITRT-CR6l4OsK.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-C0DFTCS6.js → flowDiagram-23GEKE2U-4y1ts4PX.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-COqaqM45.js → ganttDiagram-NO4QXBWP-CyKdJ2e9.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-Cwb9oGjZ.js → gitGraphDiagram-IHSO6WYX-Ct5ShF3i.js} +1 -1
- package/web/dist/assets/index-DaRnh5NC.css +1 -0
- package/web/dist/assets/index-aFRNNWe9.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CdPnS1-j.js → infoDiagram-FWYZ7A6U-uWoTXOM3.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-D4Muo2uw.js → ishikawaDiagram-FXEZZL3T-BTlDV_pc.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-0uVdJDiB.js → journeyDiagram-5HDEW3XC-_R-XB78s.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DWQU6DYI.js → kanban-definition-HUTT4EX6-D7iwBjoS.js} +1 -1
- package/web/dist/assets/{linear-BOxgX8xV.js → linear-BugN16AP.js} +1 -1
- package/web/dist/assets/{mermaid.core-C8tHn40J.js → mermaid.core-De_KIbl-.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-C8NH0hQq.js → mindmap-definition-LN4V7U3C-B72NDgcU.js} +1 -1
- package/web/dist/assets/{papaparse.min-CwI0VkgN.js → papaparse.min-BsD41u4P.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-CksEEUgN.js → pegDiagram-2B236MQR-C6oifn4x.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-Bq2Bv0cw.js → pieDiagram-ENE6RG2P-Cpn0_Hp6.js} +1 -1
- package/web/dist/assets/pilotswarm-9Szh1sEC.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-vvpwUrN5.js → quadrantDiagram-ABIIQ3AL-CJT4OoDA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-yXGWn-zZ.js → railroadDiagram-RFXS5EU6-BKtBN8XC.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-Bt1Q6Y60.js → requirementDiagram-TGXJPOKE-Dw7BCEzo.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CPNAfqgE.js → sankeyDiagram-HTMAVEWB-Avmr1rvJ.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-Dz432FWB.js → sequenceDiagram-DBY2YBRQ-BRNnslq1.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-Cokz6gNF.js → sizeCapture-X5ZJPWSS-BNmM8zTz.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-ojQCUdRE.js → stateDiagram-2N3HPSRC-CKXaP1Y6.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Dr0juUsm.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-DyTOkc1R.js → swimlanes-5IMT3BWC-Dkeka0gO.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-q-jLf0ZX.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DmuhBBl6.js → timeline-definition-FHXFAJF6-5hTTq-SL.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-gTxQuU5b.js → vennDiagram-L72KCM5P-DL8MolqK.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-YNXbSRf2.js → wardleyDiagram-EHGQE667-CwGhy6eW.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-D7loT9cv.js → xychartDiagram-FW5EYKEG-Dn4T-3Dx.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +8 -0
- package/web/server.js +8 -0
- package/web/dist/assets/channel-CIBN5O06.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-Bq-5VkDc.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-Bq-5VkDc.js +0 -1
- package/web/dist/assets/index-BOkjbFY2.js +0 -24
- package/web/dist/assets/index-DCx4DEMM.css +0 -1
- package/web/dist/assets/pilotswarm-uebRfMjg.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BliVrz4U.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DmwmYUnc.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.58",
|
|
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.58",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
@@ -1976,6 +1976,11 @@ export class NodeSdkTransport {
|
|
|
1976
1976
|
return this.mgmt.getCanvasLive(sessionId);
|
|
1977
1977
|
}
|
|
1978
1978
|
|
|
1979
|
+
async getLive(sessionId, topics) {
|
|
1980
|
+
if (typeof this.mgmt.getLive !== "function") return [];
|
|
1981
|
+
return this.mgmt.getLive(sessionId, topics);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1979
1984
|
// The canvas KV store. The portal runtime hands a resolved principal
|
|
1980
1985
|
// (door 1); the native TUI has no canvas surface and never calls these.
|
|
1981
1986
|
async readCanvasKv(sessionId, slot, principal, query = {}) {
|
|
@@ -4,8 +4,10 @@ import { parseAgentSourceLink } from "./repo-links.js";
|
|
|
4
4
|
import { importPackageFilesFromLink, readImportedPackageName } from "./repo-import.js";
|
|
5
5
|
import {
|
|
6
6
|
appendEventToHistory,
|
|
7
|
+
applyLiveTurnToHistory,
|
|
7
8
|
buildHistoryModel,
|
|
8
9
|
CHAT_HISTORY_EVENT_TYPES,
|
|
10
|
+
clearLiveTurnFromHistory,
|
|
9
11
|
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
10
12
|
dedupeChatMessages,
|
|
11
13
|
getNextHistoryEventLimit,
|
|
@@ -2503,6 +2505,8 @@ export class PilotSwarmUiController {
|
|
|
2503
2505
|
}
|
|
2504
2506
|
|
|
2505
2507
|
async stop() {
|
|
2508
|
+
for (const timer of this._liveTurnIdleTimers?.values() || []) clearTimeout(timer);
|
|
2509
|
+
this._liveTurnIdleTimers?.clear();
|
|
2506
2510
|
if (this.catalogTimer) clearInterval(this.catalogTimer);
|
|
2507
2511
|
this.catalogTimer = null;
|
|
2508
2512
|
if (this.activeSessionDetailTimer) clearTimeout(this.activeSessionDetailTimer);
|
|
@@ -2518,6 +2522,16 @@ export class PilotSwarmUiController {
|
|
|
2518
2522
|
}
|
|
2519
2523
|
|
|
2520
2524
|
detachActiveSession() {
|
|
2525
|
+
// Timers belong to the subscription, not to a future visit to this session.
|
|
2526
|
+
const sessionId = this.activeSessionSubscriptionId;
|
|
2527
|
+
if (sessionId) {
|
|
2528
|
+
clearTimeout(this._liveTurnIdleTimers?.get(sessionId));
|
|
2529
|
+
this._liveTurnIdleTimers?.delete(sessionId);
|
|
2530
|
+
const history = this.getState().history.bySessionId.get(sessionId);
|
|
2531
|
+
if (history) this.dispatch({ type: "history/set", sessionId, history: {
|
|
2532
|
+
...history, chat: history.chat.filter((item) => !item.liveTurn),
|
|
2533
|
+
} });
|
|
2534
|
+
}
|
|
2521
2535
|
if (this.activeSessionUnsub) {
|
|
2522
2536
|
this.activeSessionUnsub();
|
|
2523
2537
|
this.activeSessionUnsub = null;
|
|
@@ -2866,8 +2880,9 @@ export class PilotSwarmUiController {
|
|
|
2866
2880
|
if (newEvents.length === 0) {
|
|
2867
2881
|
return existingHistory;
|
|
2868
2882
|
}
|
|
2869
|
-
let history = existingHistory;
|
|
2883
|
+
let history = this.getState().history.bySessionId.get(sessionId) || existingHistory;
|
|
2870
2884
|
for (const event of newEvents) {
|
|
2885
|
+
if (event.seq <= (history.lastSeq || 0)) continue;
|
|
2871
2886
|
history = appendEventToHistory(history, event);
|
|
2872
2887
|
}
|
|
2873
2888
|
history = {
|
|
@@ -2937,10 +2952,37 @@ export class PilotSwarmUiController {
|
|
|
2937
2952
|
}
|
|
2938
2953
|
throw error;
|
|
2939
2954
|
}
|
|
2940
|
-
const
|
|
2941
|
-
|
|
2942
|
-
|
|
2955
|
+
const current = this.getState().history.bySessionId.get(sessionId);
|
|
2956
|
+
// Durable events and previews can arrive while REST is pending.
|
|
2957
|
+
// Merge that newer truth before replacing the loaded window.
|
|
2958
|
+
const mergedEvents = [...new Map([...(events || []), ...(current?.events || [])]
|
|
2959
|
+
.map((event) => [event.seq, event])).values()].sort((a, b) => a.seq - b.seq);
|
|
2960
|
+
let history = {
|
|
2961
|
+
...buildHistoryModel(mergedEvents, { requestedLimit }),
|
|
2962
|
+
lastSeq: mergedEvents.at(-1)?.seq || 0,
|
|
2943
2963
|
};
|
|
2964
|
+
history.closedLiveKeys = current?.closedLiveKeys || [];
|
|
2965
|
+
history.closedLiveStreams = current?.closedLiveStreams || [];
|
|
2966
|
+
const newTerminal = mergedEvents.some((event) => event.seq > (current?.lastSeq || 0)
|
|
2967
|
+
&& ["session.turn_completed", "session.turn_stopped"].includes(event.eventType));
|
|
2968
|
+
for (const item of current?.chat || []) {
|
|
2969
|
+
if (item.streamSettling) {
|
|
2970
|
+
history.chat = history.chat.map((message) => message.messageId === item.messageId
|
|
2971
|
+
? { ...item, ...message } : message);
|
|
2972
|
+
} else if (item.liveTurn && !newTerminal) {
|
|
2973
|
+
// Reuse the item itself: its key, reveal clock and memoized
|
|
2974
|
+
// rendering belong to this visit, not the REST response.
|
|
2975
|
+
if (!history.chat.some((message) => message.messageId && message.messageId === item.messageId)) {
|
|
2976
|
+
history.chat.push(item);
|
|
2977
|
+
} else {
|
|
2978
|
+
const finalEvent = mergedEvents.find((event) => event.eventType === "assistant.message" && event.data?.messageId === item.messageId);
|
|
2979
|
+
if (finalEvent) {
|
|
2980
|
+
const reconciled = appendEventToHistory({ chat: [item], events: [], activity: [], lastSeq: 0 }, finalEvent).chat[0];
|
|
2981
|
+
history.chat = history.chat.map((message) => message.messageId === item.messageId ? { ...reconciled, ...message } : message);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2944
2986
|
this.dispatch({
|
|
2945
2987
|
type: "history/set",
|
|
2946
2988
|
sessionId,
|
|
@@ -5708,6 +5750,71 @@ export class PilotSwarmUiController {
|
|
|
5708
5750
|
// history would corrupt the replay cursor — and they must never
|
|
5709
5751
|
// appear in the transcript.
|
|
5710
5752
|
if (event.transient === true) {
|
|
5753
|
+
if (event.eventType === "assistant.live_tick") {
|
|
5754
|
+
// A worker can die leaving a reasoning-only retained row.
|
|
5755
|
+
// On re-entry its message ID cannot match a durable final;
|
|
5756
|
+
// use database timestamps to reject snapshots older than an
|
|
5757
|
+
// already-loaded terminal event. Never use client wall time.
|
|
5758
|
+
const snapshotTime = event.liveUpdatedAt ? new Date(event.liveUpdatedAt).getTime() : NaN;
|
|
5759
|
+
if (event.data?.phase === "live" && Number.isFinite(snapshotTime)) {
|
|
5760
|
+
const history = this.getState().history.bySessionId.get(sessionId);
|
|
5761
|
+
if (history?.events?.some((entry) => ["session.turn_completed", "session.turn_stopped"].includes(entry.eventType)
|
|
5762
|
+
&& new Date(entry.createdAt).getTime() > snapshotTime)) return false;
|
|
5763
|
+
}
|
|
5764
|
+
if (!this._liveTurnIdleTimers) this._liveTurnIdleTimers = new Map();
|
|
5765
|
+
const pendingIdle = this._liveTurnIdleTimers.get(sessionId);
|
|
5766
|
+
if (pendingIdle && event.data?.phase === "idle") {
|
|
5767
|
+
clearTimeout(pendingIdle);
|
|
5768
|
+
this._liveTurnIdleTimers.delete(sessionId);
|
|
5769
|
+
}
|
|
5770
|
+
// The live plane and durable event stream are independent.
|
|
5771
|
+
// An idle tick commonly wins the race by a few milliseconds;
|
|
5772
|
+
// tearing down immediately makes the live answer flash out
|
|
5773
|
+
// before its durable replacement lands. Give reconciliation a
|
|
5774
|
+
// bounded grace window, then clear only if it is still live.
|
|
5775
|
+
if (event.data?.phase === "idle") {
|
|
5776
|
+
const ended = this.getState().history.bySessionId.get(sessionId);
|
|
5777
|
+
if (!ended) return true;
|
|
5778
|
+
const streamId = event.data?.streamId || null;
|
|
5779
|
+
const endedKeys = new Set(ended.chat.filter((item) => item.liveTurn && (
|
|
5780
|
+
!streamId || item.streamId === streamId
|
|
5781
|
+
|| (ended.closedLiveStreams || []).includes(item.streamId)
|
|
5782
|
+
|| (ended.closedLiveKeys || []).includes(item.liveKey)
|
|
5783
|
+
)).map((item) => item.liveKey));
|
|
5784
|
+
// Never reveal a preview that has already finished. Keep
|
|
5785
|
+
// only previews that crossed the reveal threshold, and
|
|
5786
|
+
// close their keys now so late packets cannot revive them.
|
|
5787
|
+
this.dispatch({ type: "history/set", sessionId, history: {
|
|
5788
|
+
...clearLiveTurnFromHistory(ended, null, streamId),
|
|
5789
|
+
chat: ended.chat.filter((item) => !item.liveTurn || !endedKeys.has(item.liveKey) || Date.now() - item.liveStartedAt >= 200),
|
|
5790
|
+
} });
|
|
5791
|
+
const timer = setTimeout(() => {
|
|
5792
|
+
this._liveTurnIdleTimers?.delete(sessionId);
|
|
5793
|
+
const current = this.getState().history.bySessionId.get(sessionId);
|
|
5794
|
+
if (!current) return;
|
|
5795
|
+
const chat = current.chat.filter((item) => !item.liveTurn || !endedKeys.has(item.liveKey));
|
|
5796
|
+
const cleared = chat.length === current.chat.length ? current : { ...current, chat };
|
|
5797
|
+
if (cleared !== current) {
|
|
5798
|
+
this.dispatch({ type: "history/set", sessionId, history: cleared });
|
|
5799
|
+
}
|
|
5800
|
+
}, 1_200);
|
|
5801
|
+
timer.unref?.();
|
|
5802
|
+
this._liveTurnIdleTimers.set(sessionId, timer);
|
|
5803
|
+
return true;
|
|
5804
|
+
}
|
|
5805
|
+
const existing = this.getState().history.bySessionId.get(sessionId)
|
|
5806
|
+
|| { chat: [], activity: [], events: [], lastSeq: 0 };
|
|
5807
|
+
this.dispatch({
|
|
5808
|
+
type: "history/set",
|
|
5809
|
+
sessionId,
|
|
5810
|
+
history: applyLiveTurnToHistory(existing, event.data, {
|
|
5811
|
+
sessionId,
|
|
5812
|
+
seq: event.liveSeq,
|
|
5813
|
+
createdAt: Date.now(),
|
|
5814
|
+
}),
|
|
5815
|
+
});
|
|
5816
|
+
return true;
|
|
5817
|
+
}
|
|
5711
5818
|
if (event.eventType === "session.canvas_data") {
|
|
5712
5819
|
this.applyCanvasDataEvent(sessionId, event);
|
|
5713
5820
|
}
|
|
@@ -5730,6 +5837,12 @@ export class PilotSwarmUiController {
|
|
|
5730
5837
|
}
|
|
5731
5838
|
return false;
|
|
5732
5839
|
}
|
|
5840
|
+
if (this._liveTurnIdleTimers?.has(sessionId)
|
|
5841
|
+
&& (event.eventType === "session.turn_completed"
|
|
5842
|
+
|| event.eventType === "session.turn_stopped")) {
|
|
5843
|
+
clearTimeout(this._liveTurnIdleTimers.get(sessionId));
|
|
5844
|
+
this._liveTurnIdleTimers.delete(sessionId);
|
|
5845
|
+
}
|
|
5733
5846
|
const state = this.getState();
|
|
5734
5847
|
const existing = state.history.bySessionId.get(sessionId) || { chat: [], activity: [], lastSeq: 0 };
|
|
5735
5848
|
if (event.seq <= (existing.lastSeq || 0)) return false;
|
package/ui/core/src/history.js
CHANGED
|
@@ -530,6 +530,9 @@ function buildChatMessage(event, role) {
|
|
|
530
530
|
const attachments = Array.isArray(event?.data?.attachments)
|
|
531
531
|
? event.data.attachments.filter((a) => a && typeof a.filename === "string" && a.filename)
|
|
532
532
|
: [];
|
|
533
|
+
const messageId = typeof event?.data?.messageId === "string" && event.data.messageId
|
|
534
|
+
? event.data.messageId
|
|
535
|
+
: null;
|
|
533
536
|
return {
|
|
534
537
|
id: `${event.sessionId}:${event.seq}`,
|
|
535
538
|
role: deriveChatRole(event, role, text),
|
|
@@ -539,9 +542,76 @@ function buildChatMessage(event, role) {
|
|
|
539
542
|
...(clientMessageIds.length > 0 ? { clientMessageIds } : {}),
|
|
540
543
|
...(sender ? { sender } : {}),
|
|
541
544
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
545
|
+
...(messageId ? { messageId } : {}),
|
|
542
546
|
};
|
|
543
547
|
}
|
|
544
548
|
|
|
549
|
+
/** Apply one transient `turn` topic snapshot without touching durable events/seq. */
|
|
550
|
+
export function applyLiveTurnToHistory(history, payload, meta = {}) {
|
|
551
|
+
if (!payload) return history;
|
|
552
|
+
if (payload.phase === "idle") return clearLiveTurnFromHistory(history, null, payload.streamId || null);
|
|
553
|
+
if (payload.phase !== "live") return history;
|
|
554
|
+
const messageId = typeof payload.messageId === "string" && payload.messageId ? payload.messageId : null;
|
|
555
|
+
const reasoningId = typeof payload.reasoningId === "string" && payload.reasoningId ? payload.reasoningId : null;
|
|
556
|
+
const liveKey = messageId || (reasoningId ? `reasoning:${reasoningId}` : "active-turn");
|
|
557
|
+
const createdAt = Number(meta.createdAt) || Date.now();
|
|
558
|
+
const current = history || { chat: [], activity: [], events: [], lastSeq: 0 };
|
|
559
|
+
const oldChat = current.chat || [];
|
|
560
|
+
// Durable truth wins even when a delayed retained snapshot arrives later.
|
|
561
|
+
if ((messageId && oldChat.some((item) => !item.liveTurn && item.messageId === messageId))
|
|
562
|
+
|| (current.closedLiveKeys || []).includes(liveKey)
|
|
563
|
+
|| (payload.streamId && (current.closedLiveStreams || []).includes(payload.streamId))) return current;
|
|
564
|
+
const matches = (item) => item?.liveTurn === true && (
|
|
565
|
+
item?.liveKey === liveKey
|
|
566
|
+
|| (messageId && item?.messageId === messageId)
|
|
567
|
+
|| (reasoningId && item?.reasoningId === reasoningId && (!item.messageId || !messageId))
|
|
568
|
+
);
|
|
569
|
+
const firstMatch = oldChat.findIndex(matches);
|
|
570
|
+
const previous = firstMatch >= 0 ? oldChat[firstMatch] : null;
|
|
571
|
+
const chat = oldChat.filter((item) => !matches(item));
|
|
572
|
+
const reasoningText = typeof payload.reasoningText === "string" ? payload.reasoningText : "";
|
|
573
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
574
|
+
const liveStartedAt = Number(previous?.liveStartedAt || previous?.createdAt) || createdAt;
|
|
575
|
+
// Reasoning and answer text are one visual turn, not two independently
|
|
576
|
+
// mounted transcript rows. Keeping them in one keyed item lets the browser
|
|
577
|
+
// collapse reasoning above the answer and lets the durable final settle
|
|
578
|
+
// into the exact same React component instead of flashing between nodes.
|
|
579
|
+
const replacement = (reasoningText || text) ? [{
|
|
580
|
+
id: previous?.id || `live:${meta.sessionId || ""}:${liveKey}`,
|
|
581
|
+
role: "assistant",
|
|
582
|
+
...(messageId ? { messageId } : {}),
|
|
583
|
+
...(reasoningId ? { reasoningId } : {}),
|
|
584
|
+
liveKey: previous?.liveKey || liveKey,
|
|
585
|
+
liveTurn: true,
|
|
586
|
+
streamId: payload.streamId || previous?.streamId || null,
|
|
587
|
+
streaming: true,
|
|
588
|
+
truncated: payload.truncated === true,
|
|
589
|
+
text,
|
|
590
|
+
reasoningText,
|
|
591
|
+
liveStartedAt,
|
|
592
|
+
createdAt: liveStartedAt,
|
|
593
|
+
time: "",
|
|
594
|
+
}] : [];
|
|
595
|
+
chat.splice(firstMatch < 0 ? chat.length : Math.min(firstMatch, chat.length), 0, ...replacement);
|
|
596
|
+
return { ...current, chat };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function clearLiveTurnFromHistory(history, messageId = null, streamId = null) {
|
|
600
|
+
if (!history?.chat) return history;
|
|
601
|
+
const chat = history.chat.filter((item) => !(
|
|
602
|
+
item?.liveTurn === true && (!messageId || item?.messageId === messageId) && (!streamId || item?.streamId === streamId)
|
|
603
|
+
));
|
|
604
|
+
const retired = history.chat.filter((item) => item.liveTurn && (!messageId || item.messageId === messageId) && (!streamId || item.streamId === streamId));
|
|
605
|
+
const closedLiveKeys = [...new Set([...(history.closedLiveKeys || []),
|
|
606
|
+
...retired.flatMap((item) => [item.liveKey, item.messageId, item.reasoningId && `reasoning:${item.reasoningId}`].filter(Boolean)),
|
|
607
|
+
])].slice(-256);
|
|
608
|
+
const closedLiveStreams = [...new Set([...(history.closedLiveStreams || []),
|
|
609
|
+
...(!messageId ? retired.map((item) => item.streamId).filter(Boolean) : []),
|
|
610
|
+
...(streamId ? [streamId] : []),
|
|
611
|
+
])].slice(-256);
|
|
612
|
+
return chat.length === history.chat.length && !streamId ? history : { ...history, chat, closedLiveKeys, closedLiveStreams };
|
|
613
|
+
}
|
|
614
|
+
|
|
545
615
|
// The Copilot SDK echoes the system prompt as a system.message on EVERY
|
|
546
616
|
// turn, and the worker keeps only a 120-char snippet of it (session-proxy.ts,
|
|
547
617
|
// "Full content omitted from CMS"). For one release (0.5.47) each echo also
|
|
@@ -765,6 +835,12 @@ function formatActivity(event) {
|
|
|
765
835
|
case "assistant.streaming_progress":
|
|
766
836
|
return null;
|
|
767
837
|
|
|
838
|
+
case "model.call_start": {
|
|
839
|
+
const model = String(event?.data?.model || event?.data?.modelId || "model").split(":").pop();
|
|
840
|
+
runs = buildLabeledActivityRuns(time, "[model]", "cyan", `calling ${model}`, "white");
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
|
|
768
844
|
case "session.artifact_presented": {
|
|
769
845
|
const data = (event?.data ?? {}) || {};
|
|
770
846
|
const filename = String(data.filename || "artifact");
|
|
@@ -1167,6 +1243,8 @@ export function appendEventToHistory(history, event) {
|
|
|
1167
1243
|
? existingEvents
|
|
1168
1244
|
: [...existingEvents, event].slice(-loadedEventLimit);
|
|
1169
1245
|
const next = {
|
|
1246
|
+
closedLiveKeys: history?.closedLiveKeys || [],
|
|
1247
|
+
closedLiveStreams: history?.closedLiveStreams || [],
|
|
1170
1248
|
chat: clampHistoryItems(history?.chat || [], loadedEventLimit),
|
|
1171
1249
|
activity: clampHistoryItems(history?.activity || [], loadedEventLimit),
|
|
1172
1250
|
events: nextEvents,
|
|
@@ -1201,6 +1279,7 @@ export function appendEventToHistory(history, event) {
|
|
|
1201
1279
|
: m
|
|
1202
1280
|
));
|
|
1203
1281
|
}
|
|
1282
|
+
Object.assign(next, clearLiveTurnFromHistory(next));
|
|
1204
1283
|
return next;
|
|
1205
1284
|
}
|
|
1206
1285
|
|
|
@@ -1241,10 +1320,42 @@ export function appendEventToHistory(history, event) {
|
|
|
1241
1320
|
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
1242
1321
|
const message = buildChatMessage(event, "assistant");
|
|
1243
1322
|
if (!message) return next;
|
|
1244
|
-
|
|
1323
|
+
const liveIndex = message.messageId
|
|
1324
|
+
? next.chat.findIndex((item) => item?.liveTurn === true && item?.messageId === message.messageId)
|
|
1325
|
+
: -1;
|
|
1326
|
+
const liveItem = liveIndex >= 0 ? next.chat[liveIndex] : null;
|
|
1327
|
+
Object.assign(next, clearLiveTurnFromHistory(next, message.messageId || null));
|
|
1328
|
+
// Preserve the model-call slot when several assistant messages are
|
|
1329
|
+
// interleaved: the durable final replaces its own live bubble in
|
|
1330
|
+
// place instead of jumping behind a later streaming message.
|
|
1331
|
+
if (liveIndex >= 0) {
|
|
1332
|
+
const settledAt = Date.now();
|
|
1333
|
+
const liveStartedAt = Number(liveItem?.liveStartedAt || liveItem?.createdAt) || settledAt;
|
|
1334
|
+
const liveAgeMs = Math.max(0, settledAt - liveStartedAt);
|
|
1335
|
+
// A live surface that never crossed the reveal threshold remains
|
|
1336
|
+
// invisible and the durable answer renders normally. Once it was
|
|
1337
|
+
// eligible to be seen, preserve the key and reasoning long enough
|
|
1338
|
+
// for the browser to morph it smoothly into committed transcript.
|
|
1339
|
+
if (liveAgeMs >= 200) {
|
|
1340
|
+
const streamSettleDelayMs = Math.max(0, 900 - liveAgeMs);
|
|
1341
|
+
Object.assign(message, {
|
|
1342
|
+
streamSettling: true,
|
|
1343
|
+
liveKey: liveItem?.liveKey || message.messageId,
|
|
1344
|
+
liveStartedAt,
|
|
1345
|
+
liveReasoningText: liveItem?.reasoningText || "",
|
|
1346
|
+
streamSettleDelayMs,
|
|
1347
|
+
streamSettleAt: settledAt + streamSettleDelayMs + 180,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
next.chat.splice(Math.min(liveIndex, next.chat.length), 0, message);
|
|
1351
|
+
}
|
|
1352
|
+
else next.chat.push(message);
|
|
1245
1353
|
next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
|
|
1246
1354
|
return next;
|
|
1247
1355
|
}
|
|
1356
|
+
if (event.eventType === "session.turn_completed" || event.eventType === "session.turn_stopped") {
|
|
1357
|
+
Object.assign(next, clearLiveTurnFromHistory(next));
|
|
1358
|
+
}
|
|
1248
1359
|
if (event.eventType === "system.message") {
|
|
1249
1360
|
if (shouldRenderSystemMessageAsActivity(event)) {
|
|
1250
1361
|
const activityItem = formatActivity(event);
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { buildSessionTree, isManuallyOrderableSession } from "./session-tree.js"
|
|
|
2
2
|
import { FOCUS_REGIONS } from "./commands.js";
|
|
3
3
|
import { DEFAULT_HISTORY_EVENT_LIMIT, dedupeChatMessages } from "./history.js";
|
|
4
4
|
import { getPromptInputRows } from "./layout.js";
|
|
5
|
-
import { selectSessionRows } from "./selectors.js";
|
|
5
|
+
import { SYSTEM_OWNER_KEY, ownerKeyForOwner, selectSessionRows } from "./selectors.js";
|
|
6
6
|
import { systemSessionSortOrder } from "./system-titles.js";
|
|
7
7
|
import {
|
|
8
8
|
normalizeArtifactEntries,
|
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
BUDGET_SERIES_DAYS,
|
|
21
21
|
BUDGET_SERIES_RANGES,
|
|
22
22
|
createInitialState,
|
|
23
|
+
SESSION_VIEW_LAYOUT_KEYS,
|
|
24
|
+
defaultSessionView,
|
|
25
|
+
normalizeStoredSessionViews,
|
|
23
26
|
} from "./state.js";
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -635,6 +638,38 @@ function updateUiForSessionSelection(state, nextActiveSessionId) {
|
|
|
635
638
|
};
|
|
636
639
|
}
|
|
637
640
|
|
|
641
|
+
// Add the linked session's owner to the owner filter when that is what hides
|
|
642
|
+
// it. Returns the next sessions slice, or null when the owner filter is not
|
|
643
|
+
// the reason (an `all` filter, a system or unowned session, an owner the
|
|
644
|
+
// filter already admits) so the caller falls back to the transient exception.
|
|
645
|
+
function admitLinkedSessionOwner(state, sessions, targetId) {
|
|
646
|
+
const target = sessions.byId?.[targetId];
|
|
647
|
+
if (!target || target.isGroup || target.isSystem) return null;
|
|
648
|
+
const filter = normalizeSessionOwnerFilter(sessions.ownerFilter);
|
|
649
|
+
if (filter.all === true) return null;
|
|
650
|
+
const ownerKey = ownerKeyForOwner(target.owner);
|
|
651
|
+
if (!ownerKey || ownerKey === SYSTEM_OWNER_KEY) return null;
|
|
652
|
+
const viewerKey = ownerKeyForOwner(state.auth?.principal);
|
|
653
|
+
if (viewerKey && ownerKey === viewerKey) return null;
|
|
654
|
+
if (filter.includeShared || filter.ownerKeys.includes(ownerKey)) return null;
|
|
655
|
+
const candidate = {
|
|
656
|
+
...sessions,
|
|
657
|
+
ownerFilterExplicit: true,
|
|
658
|
+
ownerFilter: { ...filter, ownerKeys: [...filter.ownerKeys, ownerKey] },
|
|
659
|
+
ownerFilterAutoAdmitted: { sessionId: targetId, ownerKey },
|
|
660
|
+
};
|
|
661
|
+
// Prove it worked: a session hidden by something else as well (a text
|
|
662
|
+
// filter, a collapsed ancestor) must not change the filter for nothing.
|
|
663
|
+
const rows = selectSessionRows({ ...state, sessions: candidate });
|
|
664
|
+
return rows.some((row) => row.sessionId === targetId) ? candidate : null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function keepAdmittedOwner(filter, admitted) {
|
|
668
|
+
const ownerKey = admitted?.ownerKey;
|
|
669
|
+
if (!ownerKey || filter.all === true || filter.includeShared || filter.ownerKeys.includes(ownerKey)) return filter;
|
|
670
|
+
return { ...filter, ownerKeys: [...filter.ownerKeys, ownerKey] };
|
|
671
|
+
}
|
|
672
|
+
|
|
638
673
|
function applyVisibleSessionSelection(state, nextSessions) {
|
|
639
674
|
let sessions = nextSessions;
|
|
640
675
|
|
|
@@ -671,16 +706,27 @@ function applyVisibleSessionSelection(state, nextSessions) {
|
|
|
671
706
|
}
|
|
672
707
|
}
|
|
673
708
|
|
|
674
|
-
// A resolved intent target excluded by the current filters
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
709
|
+
// A resolved intent target excluded by the current filters. First choice:
|
|
710
|
+
// when the only thing hiding it is that its owner is not in the owner
|
|
711
|
+
// filter, ADD that owner to the filter. The filter persists with the
|
|
712
|
+
// profile, so the linked session — and that person's other shared
|
|
713
|
+
// sessions — stay listed after the link is gone. Without this the link
|
|
714
|
+
// opened the chat but the row never appeared in the list. Fallback (no
|
|
715
|
+
// human owner, or hidden for another reason): the transient exception,
|
|
716
|
+
// never persisted, cleared on manual navigation or filter change.
|
|
717
|
+
if (intentTargetId && sessions.filterExceptionId !== intentTargetId
|
|
718
|
+
&& sessions.ownerFilterAutoAdmitted?.sessionId !== intentTargetId) {
|
|
678
719
|
const probeRows = selectSessionRows({ ...state, sessions });
|
|
679
720
|
if (!probeRows.some((row) => row.sessionId === intentTargetId)) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
721
|
+
const admitted = admitLinkedSessionOwner(state, sessions, intentTargetId);
|
|
722
|
+
if (admitted) {
|
|
723
|
+
sessions = admitted;
|
|
724
|
+
} else {
|
|
725
|
+
sessions = {
|
|
726
|
+
...sessions,
|
|
727
|
+
filterExceptionId: intentTargetId,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
684
730
|
}
|
|
685
731
|
}
|
|
686
732
|
|
|
@@ -771,6 +817,103 @@ function normalizePromptAttachments(prompt, attachments) {
|
|
|
771
817
|
}
|
|
772
818
|
|
|
773
819
|
export function appReducer(state, action) {
|
|
820
|
+
const next = baseReducer(state, action);
|
|
821
|
+
if (next === state) return next;
|
|
822
|
+
return reconcileSessionView(state, next, action);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Per-session desktop views, kept in step with the ui slice.
|
|
826
|
+
//
|
|
827
|
+
// active session changed → apply that session's stored view (or the
|
|
828
|
+
// default: no columns, even sizes)
|
|
829
|
+
// profile settings applied → re-apply the active session's view, so the
|
|
830
|
+
// global desktopPanes/layoutAdjustments a poll
|
|
831
|
+
// carries never override a per-session choice
|
|
832
|
+
// anything else changed a column toggle or a right-side size while the
|
|
833
|
+
// session stayed the same → record it for that session
|
|
834
|
+
//
|
|
835
|
+
// Desktop only (the phone has no columns; its layout is untouched), and only
|
|
836
|
+
// once the profile has been read, so the defaults cannot be recorded over a
|
|
837
|
+
// stored view the tab has not seen yet.
|
|
838
|
+
function reconcileSessionView(prev, next, action) {
|
|
839
|
+
const ui = next.ui;
|
|
840
|
+
if (ui.sessionViewDevice !== "desktop") return next;
|
|
841
|
+
const active = next.sessions.activeSessionId || null;
|
|
842
|
+
if (!active || next.sessions.byId?.[active]?.isGroup) return next;
|
|
843
|
+
const prevActive = prev.sessions.activeSessionId || null;
|
|
844
|
+
// A profile read (the first one included) re-applies the active
|
|
845
|
+
// session's view from the merged map. Local edits made before the read
|
|
846
|
+
// were recorded below and win the merge, so a column opened while the
|
|
847
|
+
// profile was still loading is not shut by the answer.
|
|
848
|
+
if (action.type === "profileSettings/apply") {
|
|
849
|
+
return { ...next, ui: applySessionViewToUi(ui, ui.sessionViews?.[active]?.desktop || defaultSessionView()) };
|
|
850
|
+
}
|
|
851
|
+
if (active !== prevActive) {
|
|
852
|
+
// Before the read there is nothing to apply on a switch; the stored
|
|
853
|
+
// view lands with the read.
|
|
854
|
+
if (!ui.sessionViewsLoaded) return next;
|
|
855
|
+
return { ...next, ui: applySessionViewToUi(ui, ui.sessionViews?.[active]?.desktop || defaultSessionView()) };
|
|
856
|
+
}
|
|
857
|
+
const current = sessionViewFromUi(ui);
|
|
858
|
+
const before = sessionViewFromUi(prev.ui);
|
|
859
|
+
if (sessionViewsEqual(current, before)) return next;
|
|
860
|
+
const entry = { ...(ui.sessionViews?.[active] || {}), desktop: { ...current, at: Date.now() } };
|
|
861
|
+
return {
|
|
862
|
+
...next,
|
|
863
|
+
ui: {
|
|
864
|
+
...ui,
|
|
865
|
+
sessionViews: { ...(ui.sessionViews || {}), [active]: entry },
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function sessionViewFromUi(ui) {
|
|
871
|
+
const layout = {};
|
|
872
|
+
for (const key of SESSION_VIEW_LAYOUT_KEYS) layout[key] = Number(ui.layout?.[key]) || 0;
|
|
873
|
+
return {
|
|
874
|
+
canvasOpen: ui.canvasOpen === true,
|
|
875
|
+
diagnosticsOpen: ui.diagnosticsOpen === true,
|
|
876
|
+
zen: ui.canvasOpen === true && ui.canvasZen === true,
|
|
877
|
+
layout,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function sessionViewsEqual(a, b) {
|
|
882
|
+
if (a.canvasOpen !== b.canvasOpen || a.diagnosticsOpen !== b.diagnosticsOpen || a.zen !== b.zen) return false;
|
|
883
|
+
for (const key of SESSION_VIEW_LAYOUT_KEYS) if (a.layout[key] !== b.layout[key]) return false;
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function applySessionViewToUi(ui, view) {
|
|
888
|
+
const current = sessionViewFromUi(ui);
|
|
889
|
+
const wanted = { canvasOpen: view.canvasOpen === true, diagnosticsOpen: view.diagnosticsOpen === true, zen: view.canvasOpen === true && view.zen === true, layout: view.layout || {} };
|
|
890
|
+
if (sessionViewsEqual(current, { ...wanted, layout: { ...current.layout, ...wanted.layout } })) return ui;
|
|
891
|
+
return {
|
|
892
|
+
...ui,
|
|
893
|
+
canvasOpen: wanted.canvasOpen,
|
|
894
|
+
diagnosticsOpen: wanted.diagnosticsOpen,
|
|
895
|
+
canvasZen: wanted.zen,
|
|
896
|
+
rightPaneMode: wanted.canvasOpen ? "canvas" : "panes",
|
|
897
|
+
...(wanted.canvasOpen ? {} : { canvasMaximized: false }),
|
|
898
|
+
layout: { ...(ui.layout || {}), ...wanted.layout },
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Newest wins, per session: every record carries the time it was made, so a
|
|
903
|
+
// local edit newer than the stored one survives the poll, and another
|
|
904
|
+
// desktop's later edit replaces this tab's older one. No "touched" set —
|
|
905
|
+
// one that never cleared made two desktops fight over the same session.
|
|
906
|
+
function mergeSessionViews(remote, local) {
|
|
907
|
+
const out = { ...(remote || {}) };
|
|
908
|
+
for (const [id, entry] of Object.entries(local || {})) {
|
|
909
|
+
const localAt = Number(entry?.desktop?.at) || 0;
|
|
910
|
+
const remoteAt = Number(out[id]?.desktop?.at) || 0;
|
|
911
|
+
if (!out[id] || localAt > remoteAt) out[id] = entry;
|
|
912
|
+
}
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function baseReducer(state, action) {
|
|
774
917
|
switch (action.type) {
|
|
775
918
|
case "connection/ready":
|
|
776
919
|
return {
|
|
@@ -828,6 +971,12 @@ export function appReducer(state, action) {
|
|
|
828
971
|
},
|
|
829
972
|
};
|
|
830
973
|
|
|
974
|
+
case "ui/sessionViewDevice": {
|
|
975
|
+
const device = action.device === "desktop" || action.device === "mobile" ? action.device : null;
|
|
976
|
+
if (device === state.ui.sessionViewDevice) return state;
|
|
977
|
+
return { ...state, ui: { ...state.ui, sessionViewDevice: device } };
|
|
978
|
+
}
|
|
979
|
+
|
|
831
980
|
case "ui/touchScale":
|
|
832
981
|
if (Boolean(action.enabled) === Boolean(state.ui.touchScale)) return state;
|
|
833
982
|
return { ...state, ui: { ...state.ui, touchScale: Boolean(action.enabled) } };
|
|
@@ -868,6 +1017,11 @@ export function appReducer(state, action) {
|
|
|
868
1017
|
// migrating that — see normalizeStoredDesktopPanes.
|
|
869
1018
|
const hasDesktopPanes = Object.prototype.hasOwnProperty.call(settings, "desktopPanes")
|
|
870
1019
|
&& settings.desktopPanes && typeof settings.desktopPanes === "object";
|
|
1020
|
+
const hasSessionViews = Object.prototype.hasOwnProperty.call(settings, "sessionViews")
|
|
1021
|
+
&& settings.sessionViews && typeof settings.sessionViews === "object";
|
|
1022
|
+
const nextSessionViews = hasSessionViews
|
|
1023
|
+
? mergeSessionViews(normalizeStoredSessionViews(settings.sessionViews), state.ui.sessionViews)
|
|
1024
|
+
: state.ui.sessionViews;
|
|
871
1025
|
const nextDesktopPanes = (hasDesktopPanes || hasRightPaneMode)
|
|
872
1026
|
? normalizeStoredDesktopPanes(
|
|
873
1027
|
hasDesktopPanes ? settings.desktopPanes : null,
|
|
@@ -914,7 +1068,13 @@ export function appReducer(state, action) {
|
|
|
914
1068
|
...state.sessions,
|
|
915
1069
|
...(hasOwnerFilter
|
|
916
1070
|
? {
|
|
917
|
-
|
|
1071
|
+
// A stored filter that predates a just-admitted owner
|
|
1072
|
+
// (the first poll racing the deep link, or another
|
|
1073
|
+
// device's save) must not drop that owner again.
|
|
1074
|
+
ownerFilter: keepAdmittedOwner(
|
|
1075
|
+
normalizeSessionOwnerFilter(settings.sessionOwnerFilter),
|
|
1076
|
+
state.sessions.ownerFilterAutoAdmitted,
|
|
1077
|
+
),
|
|
918
1078
|
ownerFilterExplicit: true,
|
|
919
1079
|
}
|
|
920
1080
|
: {}),
|
|
@@ -948,6 +1108,9 @@ export function appReducer(state, action) {
|
|
|
948
1108
|
? { canvasOpen: nextDesktopPanes.canvasOpen, diagnosticsOpen: nextDesktopPanes.diagnosticsOpen, canvasZen: nextDesktopPanes.zen === true }
|
|
949
1109
|
: {}),
|
|
950
1110
|
layout: nextLayout,
|
|
1111
|
+
sessionViews: nextSessionViews,
|
|
1112
|
+
// Read once is enough: from here on this tab records.
|
|
1113
|
+
sessionViewsLoaded: true,
|
|
951
1114
|
},
|
|
952
1115
|
canvas: hasCanvasPrefs
|
|
953
1116
|
? { ...state.canvas, prefs: mergeCanvasPrefs(state.canvas.prefs, normalizeStoredCanvasPrefs(settings.canvasPrefs), hasLoadedSessions ? state.sessions.byId : null) }
|
|
@@ -1184,6 +1347,7 @@ export function appReducer(state, action) {
|
|
|
1184
1347
|
// the deep-link latch and its transient filter exception.
|
|
1185
1348
|
navigationIntent: null,
|
|
1186
1349
|
filterExceptionId: null,
|
|
1350
|
+
ownerFilterAutoAdmitted: null,
|
|
1187
1351
|
};
|
|
1188
1352
|
const selection = applyVisibleSessionSelection(state, nextSessions);
|
|
1189
1353
|
return {
|
|
@@ -1201,6 +1365,7 @@ export function appReducer(state, action) {
|
|
|
1201
1365
|
ownerFilter: normalizeSessionOwnerFilter(action.filter),
|
|
1202
1366
|
navigationIntent: null,
|
|
1203
1367
|
filterExceptionId: null,
|
|
1368
|
+
ownerFilterAutoAdmitted: null,
|
|
1204
1369
|
};
|
|
1205
1370
|
const selection = applyVisibleSessionSelection(state, nextSessions);
|
|
1206
1371
|
return {
|
|
@@ -1221,6 +1386,7 @@ export function appReducer(state, action) {
|
|
|
1221
1386
|
listDeselected: false,
|
|
1222
1387
|
navigationIntent: { sessionId, status: "pending" },
|
|
1223
1388
|
filterExceptionId: null,
|
|
1389
|
+
ownerFilterAutoAdmitted: null,
|
|
1224
1390
|
};
|
|
1225
1391
|
const selection = applyVisibleSessionSelection(state, nextSessions);
|
|
1226
1392
|
return {
|
|
@@ -1719,7 +1885,7 @@ export function appReducer(state, action) {
|
|
|
1719
1885
|
...state.sessions,
|
|
1720
1886
|
activeSessionId: action.sessionId,
|
|
1721
1887
|
...(releasesNavigationLatch
|
|
1722
|
-
? { navigationIntent: null, filterExceptionId: null }
|
|
1888
|
+
? { navigationIntent: null, filterExceptionId: null, ownerFilterAutoAdmitted: null }
|
|
1723
1889
|
: {}),
|
|
1724
1890
|
},
|
|
1725
1891
|
ui: {
|