pilotswarm 0.5.55 → 0.5.57
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/ui/core/src/reducer.js +177 -11
- package/ui/core/src/selectors.js +75 -9
- package/ui/core/src/state.js +65 -0
- package/ui/react/src/web-app.js +89 -8
- package/web/dist/assets/{abnfDiagram-VRR7QNED-Cccg5mUx.js → abnfDiagram-VRR7QNED-CJOR2hPi.js} +1 -1
- package/web/dist/assets/{arc-DFWp1OiI.js → arc-Yh3kc_KB.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-o2B4-sJ1.js → architectureDiagram-ZJ3FMSHR-DLRlUeyk.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Cf_7Cm38.js → blockDiagram-677ZJIJ3-x_TGH_fp.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CKXuPB0d.js → c4Diagram-LMCZKHZV-C-j_bxeF.js} +1 -1
- package/web/dist/assets/channel-BIn92AzB.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CZC3HwzC.js → chunk-2Q5K7J3B-CaI5Xqwo.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-7_X1k3eB.js → chunk-32BRIVSS-Bh_Crac6.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-E2rxT4um.js → chunk-5VM5RSS4-XM-3ajKG.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BbW37HWx.js → chunk-EX3LRPZG-ZwkulfFp.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-dj0DOnPl.js → chunk-JWPE2WC7-CT3Q7T2A.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-TeWalpvK.js → chunk-MOJQB5TN-_Uc0ksg3.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-ClHcH2bN.js → chunk-RYQCIY6F-DosoId7u.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-Dy5XEnNL.js → chunk-V7JOEXUC-KRjq1cwM.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-Den_q-0P.js → chunk-VR4S4FIN-B6Ul1z3S.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DZ1s-8xB.js → chunk-XXDRQBXY-AikdVXm4.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B7ktW6XM.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B7ktW6XM.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-CKAukUTf.js → cose-bilkent-JH36ORCC-DTYwgEAa.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CM8y129F.js → cynefin-VYW2F7L2-C_8v2wIz.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-DLryvwYt.js → cynefinDiagram-TSTJHNR4-1-nw1zbQ.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-BO0LMWmO.js → dagre-VKFMJZFB-CAjwRpvT.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DiFLu7v2.js → diagram-FQU43EPY-Bg41nQw_.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DMkq9we0.js → diagram-G47NLZAW-Cp6P-qMn.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-CxVQL8Dq.js → diagram-NH7WQ7WH-DNdFuedg.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-B5TrTlUc.js → diagram-OA4YK3LP-DcHpELG2.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Ca38mf1v.js → diagram-WEI45ONY-B2_kyMw3.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DZR_44LD.js → ebnfDiagram-CCIWWBDH-DztuAo3o.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-Ca8mmsU1.js → erDiagram-Q63AITRT-DNjKTOzp.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-C0DFTCS6.js → flowDiagram-23GEKE2U-DWjfeUM1.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-COqaqM45.js → ganttDiagram-NO4QXBWP-Y0MgGVmy.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-Cwb9oGjZ.js → gitGraphDiagram-IHSO6WYX--lDKAKVz.js} +1 -1
- package/web/dist/assets/{index-DCx4DEMM.css → index-BxPfp0AO.css} +1 -1
- package/web/dist/assets/{index-B91BmvYM.js → index-rPji8Uud.js} +2 -2
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CdPnS1-j.js → infoDiagram-FWYZ7A6U-CKmH8_YO.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-D4Muo2uw.js → ishikawaDiagram-FXEZZL3T-D88R7zUc.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-0uVdJDiB.js → journeyDiagram-5HDEW3XC-Ck7EMrsY.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DWQU6DYI.js → kanban-definition-HUTT4EX6-hc-mOfSG.js} +1 -1
- package/web/dist/assets/{linear-BOxgX8xV.js → linear-BVhU4csP.js} +1 -1
- package/web/dist/assets/{mermaid.core-C8tHn40J.js → mermaid.core-C9u4FOCX.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-C8NH0hQq.js → mindmap-definition-LN4V7U3C-DatQu9Xp.js} +1 -1
- package/web/dist/assets/{papaparse.min-CwI0VkgN.js → papaparse.min-BdnoXc8M.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-CksEEUgN.js → pegDiagram-2B236MQR-Cr_nZ_jR.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-Bq2Bv0cw.js → pieDiagram-ENE6RG2P-CcAbSIcY.js} +1 -1
- package/web/dist/assets/pilotswarm--kvfB_6q.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-vvpwUrN5.js → quadrantDiagram-ABIIQ3AL-CgJw5laM.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-yXGWn-zZ.js → railroadDiagram-RFXS5EU6-CHeun9SN.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-Bt1Q6Y60.js → requirementDiagram-TGXJPOKE-D1kpDj8W.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CPNAfqgE.js → sankeyDiagram-HTMAVEWB-C5vUSUaX.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-Dz432FWB.js → sequenceDiagram-DBY2YBRQ-T1aPSxk1.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-Cokz6gNF.js → sizeCapture-X5ZJPWSS-j-wh2LVX.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-ojQCUdRE.js → stateDiagram-2N3HPSRC-DfUZZR8N.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-ilKtTdHG.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-DyTOkc1R.js → swimlanes-5IMT3BWC-OpASJiRw.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-C1PthnB4.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DmuhBBl6.js → timeline-definition-FHXFAJF6-BBnc_z1U.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-gTxQuU5b.js → vennDiagram-L72KCM5P-BlPQe9p4.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-YNXbSRf2.js → wardleyDiagram-EHGQE667-DBcBFUx2.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-D7loT9cv.js → xychartDiagram-FW5EYKEG-BQUSGL6g.js} +1 -1
- package/web/dist/index.html +3 -3
- 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/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.57",
|
|
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.57",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
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: {
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -383,13 +383,13 @@ function sessionStatusIcon(session, mode = "local") {
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
function ownerKeyForOwner(owner) {
|
|
386
|
+
export function ownerKeyForOwner(owner) {
|
|
387
387
|
const provider = String(owner?.provider || "").trim();
|
|
388
388
|
const subject = String(owner?.subject || "").trim();
|
|
389
389
|
return provider && subject ? `${provider}\u0001${subject}` : null;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
function ownerDisplayName(owner, fallback = "unknown user") {
|
|
392
|
+
export function ownerDisplayName(owner, fallback = "unknown user") {
|
|
393
393
|
return String(owner?.displayName || owner?.email || "").trim() || fallback;
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -398,7 +398,7 @@ function ownerDisplayName(owner, fallback = "unknown user") {
|
|
|
398
398
|
// deletable) — but they belong to the same "System" bucket as the real system
|
|
399
399
|
// agents, so the single System filter entry covers both. Computed via the same
|
|
400
400
|
// join helper so it stays byte-identical to real owner keys.
|
|
401
|
-
const SYSTEM_OWNER_KEY = ownerKeyForOwner({ provider: "system", subject: "system" });
|
|
401
|
+
export const SYSTEM_OWNER_KEY = ownerKeyForOwner({ provider: "system", subject: "system" });
|
|
402
402
|
|
|
403
403
|
function initialsFromText(value) {
|
|
404
404
|
const text = String(value || "").trim();
|
|
@@ -1457,6 +1457,17 @@ export function selectNavigationError(state) {
|
|
|
1457
1457
|
* linked session is being shown despite the current filters excluding it.
|
|
1458
1458
|
*/
|
|
1459
1459
|
export function selectSessionFilterExceptionNotice(state) {
|
|
1460
|
+
// A linked session whose owner was added to the filter so it is listed.
|
|
1461
|
+
// Durable (the filter itself changed and persists); the notice is what
|
|
1462
|
+
// is transient, cleared on the next manual navigation or filter change.
|
|
1463
|
+
const admitted = state.sessions?.ownerFilterAutoAdmitted || null;
|
|
1464
|
+
const stillAdmitted = admitted?.ownerKey
|
|
1465
|
+
&& Array.isArray(state.sessions?.ownerFilter?.ownerKeys)
|
|
1466
|
+
&& state.sessions.ownerFilter.ownerKeys.includes(admitted.ownerKey);
|
|
1467
|
+
if (admitted?.sessionId && stillAdmitted && state.sessions?.byId?.[admitted.sessionId]) {
|
|
1468
|
+
const owner = state.sessions.byId[admitted.sessionId]?.owner;
|
|
1469
|
+
return `Added ${ownerDisplayName(owner, "its owner")} to your session filters so the linked session is listed.`;
|
|
1470
|
+
}
|
|
1460
1471
|
const exceptionId = state.sessions?.filterExceptionId || null;
|
|
1461
1472
|
if (!exceptionId || !state.sessions?.byId?.[exceptionId]) return null;
|
|
1462
1473
|
return "Showing linked session outside your current filters.";
|
|
@@ -1471,7 +1482,26 @@ export function canStopSessionTurn(session) {
|
|
|
1471
1482
|
return (session.status || "") === "running";
|
|
1472
1483
|
}
|
|
1473
1484
|
|
|
1474
|
-
|
|
1485
|
+
// The moment an event of one of these types was recorded, in ms, or null.
|
|
1486
|
+
// Product cards (Question, Warning) take their timestamp from the event that
|
|
1487
|
+
// raised them. They used to stamp session.updatedAt, which moves on EVERY
|
|
1488
|
+
// poll and status tick — so the card's clock jumped forward with each update
|
|
1489
|
+
// and read as flicker.
|
|
1490
|
+
function latestEventCreatedAtMs(events = [], eventTypes = []) {
|
|
1491
|
+
const wanted = new Set(eventTypes);
|
|
1492
|
+
for (let index = (events || []).length - 1; index >= 0; index -= 1) {
|
|
1493
|
+
const event = events[index];
|
|
1494
|
+
if (!event || !wanted.has(event.eventType)) continue;
|
|
1495
|
+
const createdAt = event.createdAt;
|
|
1496
|
+
const ms = createdAt instanceof Date
|
|
1497
|
+
? createdAt.getTime()
|
|
1498
|
+
: typeof createdAt === "number" ? createdAt : new Date(createdAt || 0).getTime();
|
|
1499
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
1500
|
+
}
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function buildPendingQuestionMessage(session, events = []) {
|
|
1475
1505
|
const pendingQuestion = session?.pendingQuestion;
|
|
1476
1506
|
if (!pendingQuestion?.question) return null;
|
|
1477
1507
|
|
|
@@ -1500,7 +1530,7 @@ function buildPendingQuestionMessage(session) {
|
|
|
1500
1530
|
role: "system",
|
|
1501
1531
|
text: body.join("\n"),
|
|
1502
1532
|
time: "",
|
|
1503
|
-
createdAt: session.
|
|
1533
|
+
createdAt: latestEventCreatedAtMs(events, ["session.input_required_started"]),
|
|
1504
1534
|
cardTitle: "Question",
|
|
1505
1535
|
cardTitleColor: "cyan",
|
|
1506
1536
|
cardBorderColor: "cyan",
|
|
@@ -1558,7 +1588,7 @@ function chatAlreadyContainsPendingQuestion(chat, question) {
|
|
|
1558
1588
|
});
|
|
1559
1589
|
}
|
|
1560
1590
|
|
|
1561
|
-
function buildSessionErrorMessage(session) {
|
|
1591
|
+
function buildSessionErrorMessage(session, events = []) {
|
|
1562
1592
|
const errorText = String(session?.error || "").trim();
|
|
1563
1593
|
if (!errorText) return null;
|
|
1564
1594
|
|
|
@@ -1575,7 +1605,10 @@ function buildSessionErrorMessage(session) {
|
|
|
1575
1605
|
role: "system",
|
|
1576
1606
|
text: body,
|
|
1577
1607
|
time: "",
|
|
1578
|
-
|
|
1608
|
+
// The error event when one was recorded, else the end of the turn
|
|
1609
|
+
// that produced the error; never the session's rolling updatedAt.
|
|
1610
|
+
createdAt: latestEventCreatedAtMs(events, ["session.error"])
|
|
1611
|
+
?? latestEventCreatedAtMs(events, ["session.turn_completed", "assistant.turn_end"]),
|
|
1579
1612
|
cardTitle: isFailed ? "Error" : "Warning",
|
|
1580
1613
|
cardTitleColor: isFailed ? "red" : "yellow",
|
|
1581
1614
|
cardBorderColor: isFailed ? "red" : "yellow",
|
|
@@ -1801,12 +1834,13 @@ export function selectActiveChat(state) {
|
|
|
1801
1834
|
}
|
|
1802
1835
|
const history = state.history.bySessionId.get(sessionId);
|
|
1803
1836
|
const chat = history?.chat || [];
|
|
1837
|
+
const events = history?.events || [];
|
|
1804
1838
|
const pendingQuestionMessage = session?.pendingQuestion?.question
|
|
1805
1839
|
&& !chatAlreadyContainsPendingQuestion(chat, session.pendingQuestion.question)
|
|
1806
|
-
? buildPendingQuestionMessage(session)
|
|
1840
|
+
? buildPendingQuestionMessage(session, events)
|
|
1807
1841
|
: null;
|
|
1808
1842
|
const answeredQuestionMessage = buildAnsweredPendingQuestionMessage(session, chat);
|
|
1809
|
-
const sessionErrorMessage = buildSessionErrorMessage(session);
|
|
1843
|
+
const sessionErrorMessage = buildSessionErrorMessage(session, events);
|
|
1810
1844
|
|
|
1811
1845
|
if ((!history || chat.length === 0) && !pendingQuestionMessage && !answeredQuestionMessage && !sessionErrorMessage) {
|
|
1812
1846
|
return createSplashCard(state.branding, session);
|
|
@@ -2949,7 +2983,39 @@ export function sessionCanvasMark(state, sessionId) {
|
|
|
2949
2983
|
return mark;
|
|
2950
2984
|
}
|
|
2951
2985
|
|
|
2986
|
+
// Memoized on its five inputs. The view carries a fresh `slots` array, so
|
|
2987
|
+
// without this every shallow-equal subscriber (the app root, the toolbar)
|
|
2988
|
+
// saw a "change" on EVERY dispatch — a keystroke, a poll, a live event —
|
|
2989
|
+
// and re-rendered the whole tree under it. Measured on a phone viewport
|
|
2990
|
+
// with a 600-turn transcript: ~10 component renders per keystroke, down to
|
|
2991
|
+
// the composer alone once the identity held.
|
|
2992
|
+
let canvasViewCache = null;
|
|
2952
2993
|
export function selectCanvasView(state) {
|
|
2994
|
+
const sessionId = state?.sessions?.activeSessionId || null;
|
|
2995
|
+
const bySessionId = state?.canvas?.bySessionId;
|
|
2996
|
+
const prefs = state?.canvas?.prefs;
|
|
2997
|
+
const canvasSlot = state?.ui?.canvasSlot;
|
|
2998
|
+
const canvasOpen = state?.ui?.canvasOpen;
|
|
2999
|
+
// flipSeq is the agent-driven "open the canvas now" tick. It changes
|
|
3000
|
+
// without any other key moving (a redraw of the already-open slot), and
|
|
3001
|
+
// the phone opens its canvas tab off it — so it MUST be a cache key.
|
|
3002
|
+
const flipSeq = state?.canvas?.flipSeq;
|
|
3003
|
+
const cached = canvasViewCache;
|
|
3004
|
+
if (cached
|
|
3005
|
+
&& cached.sessionId === sessionId
|
|
3006
|
+
&& cached.bySessionId === bySessionId
|
|
3007
|
+
&& cached.prefs === prefs
|
|
3008
|
+
&& cached.canvasSlot === canvasSlot
|
|
3009
|
+
&& cached.canvasOpen === canvasOpen
|
|
3010
|
+
&& cached.flipSeq === flipSeq) {
|
|
3011
|
+
return cached.result;
|
|
3012
|
+
}
|
|
3013
|
+
const result = computeCanvasView(state);
|
|
3014
|
+
canvasViewCache = { sessionId, bySessionId, prefs, canvasSlot, canvasOpen, flipSeq, result };
|
|
3015
|
+
return result;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
function computeCanvasView(state) {
|
|
2953
3019
|
const sessionId = state?.sessions?.activeSessionId || null;
|
|
2954
3020
|
// The pane shows ONE slot at a time. An out-of-range or never-drawn slot
|
|
2955
3021
|
// falls back to 1 so a stale selection can never strand the pane.
|
package/ui/core/src/state.js
CHANGED
|
@@ -169,6 +169,62 @@ export function normalizeStoredRightPaneMode(value) {
|
|
|
169
169
|
* "panes" -> both off. "panes" was the default value rather than a choice,
|
|
170
170
|
* and the new default is a clean two-column workspace.
|
|
171
171
|
*/
|
|
172
|
+
// ── Per-session desktop views ─────────────────────────────────────────────
|
|
173
|
+
// How a person set up the workspace for ONE session: which optional columns
|
|
174
|
+
// are open and how the right side is sized. Keyed by session id, then by
|
|
175
|
+
// device slot ("desktop" today; a "mobile" slot is reserved, not written).
|
|
176
|
+
// Absent = the default workspace: sessions and chat, no other pane.
|
|
177
|
+
export const SESSION_VIEW_LAYOUT_KEYS = Object.freeze([
|
|
178
|
+
"paneAdjust", "activityPaneAdjust", "canvasPaneAdjust", "diagnosticsPaneAdjust", "diagnosticsSplitAdjust",
|
|
179
|
+
]);
|
|
180
|
+
export const SESSION_VIEW_DEVICES = Object.freeze(["desktop", "mobile"]);
|
|
181
|
+
// Bound on the profile document: the newest entries win.
|
|
182
|
+
export const SESSION_VIEWS_MAX = 300;
|
|
183
|
+
|
|
184
|
+
export function defaultSessionView() {
|
|
185
|
+
const layout = {};
|
|
186
|
+
for (const key of SESSION_VIEW_LAYOUT_KEYS) layout[key] = 0;
|
|
187
|
+
return { canvasOpen: false, diagnosticsOpen: false, zen: false, layout, at: 0 };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeStoredSessionView(value) {
|
|
191
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
192
|
+
const view = defaultSessionView();
|
|
193
|
+
view.canvasOpen = value.canvasOpen === true;
|
|
194
|
+
view.diagnosticsOpen = value.diagnosticsOpen === true;
|
|
195
|
+
view.zen = view.canvasOpen && value.zen === true;
|
|
196
|
+
const layout = value.layout && typeof value.layout === "object" ? value.layout : {};
|
|
197
|
+
for (const key of SESSION_VIEW_LAYOUT_KEYS) {
|
|
198
|
+
const n = Number(layout[key]);
|
|
199
|
+
view.layout[key] = Number.isFinite(n) ? Math.trunc(n) : 0;
|
|
200
|
+
}
|
|
201
|
+
const at = Number(value.at);
|
|
202
|
+
view.at = Number.isFinite(at) && at > 0 ? Math.trunc(at) : 0;
|
|
203
|
+
return view;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeStoredSessionViews(value) {
|
|
207
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
208
|
+
const entries = [];
|
|
209
|
+
for (const [sessionId, slots] of Object.entries(value)) {
|
|
210
|
+
const id = String(sessionId || "").trim();
|
|
211
|
+
if (!id || !slots || typeof slots !== "object" || Array.isArray(slots)) continue;
|
|
212
|
+
const entry = {};
|
|
213
|
+
let newest = 0;
|
|
214
|
+
for (const device of SESSION_VIEW_DEVICES) {
|
|
215
|
+
const view = normalizeStoredSessionView(slots[device]);
|
|
216
|
+
if (!view) continue;
|
|
217
|
+
entry[device] = view;
|
|
218
|
+
newest = Math.max(newest, view.at);
|
|
219
|
+
}
|
|
220
|
+
if (Object.keys(entry).length > 0) entries.push([id, entry, newest]);
|
|
221
|
+
}
|
|
222
|
+
entries.sort((a, b) => b[2] - a[2]);
|
|
223
|
+
const out = {};
|
|
224
|
+
for (const [id, entry] of entries.slice(0, SESSION_VIEWS_MAX)) out[id] = entry;
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
172
228
|
export function normalizeStoredDesktopPanes(value, legacyRightPaneMode) {
|
|
173
229
|
const candidate = value && typeof value === "object" ? value : null;
|
|
174
230
|
if (candidate && (typeof candidate.canvasOpen === "boolean" || typeof candidate.diagnosticsOpen === "boolean")) {
|
|
@@ -345,6 +401,14 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
345
401
|
// a "look at this now" gesture, and returning to a portal with no
|
|
346
402
|
// chat and no session list would read as a broken app.
|
|
347
403
|
canvasMaximized: false,
|
|
404
|
+
// Per-session desktop views (see normalizeStoredSessionViews).
|
|
405
|
+
// Applied when the active session changes, recorded when the
|
|
406
|
+
// columns or the right-side sizes change — desktop only, and only
|
|
407
|
+
// once the profile has been read (so a fresh tab does not record
|
|
408
|
+
// the defaults over a stored view before it has seen it).
|
|
409
|
+
sessionViews: {},
|
|
410
|
+
sessionViewsLoaded: false,
|
|
411
|
+
sessionViewDevice: null,
|
|
348
412
|
statsViewMode: "session",
|
|
349
413
|
prompt: "",
|
|
350
414
|
promptCursor: 0,
|
|
@@ -430,6 +494,7 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
430
494
|
ownerFilter: normalizeSessionOwnerFilter(sessionOwnerFilter),
|
|
431
495
|
navigationIntent: null,
|
|
432
496
|
filterExceptionId: null,
|
|
497
|
+
ownerFilterAutoAdmitted: null,
|
|
433
498
|
},
|
|
434
499
|
history: {
|
|
435
500
|
bySessionId: new Map(),
|