pilotswarm 0.5.65 → 0.5.67
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/controller.js +47 -24
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/reducer.js +20 -5
- package/ui/core/src/selectors.js +154 -9
- package/ui/core/src/session-search.js +151 -0
- package/ui/react/src/web-app.js +114 -7
- package/web/dist/assets/{abnfDiagram-VRR7QNED-gIPHF2gV.js → abnfDiagram-VRR7QNED-rCdnr4Zk.js} +1 -1
- package/web/dist/assets/{arc-DMW_q5Ml.js → arc-DeoxxJ2S.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DAKsrxcB.js → architectureDiagram-ZJ3FMSHR-DAz2X6IV.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-BUjSLpP3.js → blockDiagram-677ZJIJ3-DPsxUKQJ.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Dw2nPoes.js → c4Diagram-LMCZKHZV-Cl8IpoAZ.js} +1 -1
- package/web/dist/assets/channel-DCTjhzIq.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-BAL28Rv0.js → chunk-2Q5K7J3B-WHxPHUqg.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-DDlt9K8s.js → chunk-32BRIVSS-9S1suaZM.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BLQGJGoP.js → chunk-5VM5RSS4-DmBwY4rK.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-XFMb3HRy.js → chunk-EX3LRPZG-BbKMT5Il.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-DWnKTQ72.js → chunk-JWPE2WC7-B9Qns4SI.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-MltzhnwP.js → chunk-MOJQB5TN-G6hkDffb.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-Ce3q0-mJ.js → chunk-RYQCIY6F-Ca38LebY.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-ThXLYY88.js → chunk-V7JOEXUC-DPeEQDsB.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DbhAykiW.js → chunk-VR4S4FIN-DYuh7YGE.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BpkNB0mD.js → chunk-XXDRQBXY-BDRBkhU_.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-Cfa6ZtJR.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-Cfa6ZtJR.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-rctqAPvj.js → cose-bilkent-JH36ORCC-CpHdEFyS.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-Dg1cltg6.js → cynefin-VYW2F7L2-DRYxiNM0.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Ci23Sv4Z.js → cynefinDiagram-TSTJHNR4-B8fSzWxH.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-BpWTGhpv.js → dagre-VKFMJZFB-CnJyU6YF.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-B69JpiAe.js → diagram-FQU43EPY-BS2_uGW0.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-BHMk1TOO.js → diagram-G47NLZAW-DjKog-bE.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-CtvZIIsk.js → diagram-NH7WQ7WH-BaWtvMDL.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BFvKh800.js → diagram-OA4YK3LP-Z11ytyEX.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-CgB8sWPe.js → diagram-WEI45ONY-DiiH4Od8.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DyqPNydM.js → ebnfDiagram-CCIWWBDH-xtE5CNkB.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-CwpvZQ3Q.js → erDiagram-Q63AITRT-Dj5wjLbd.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-EBKSKxfC.js → flowDiagram-23GEKE2U-O_1XH_-x.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-q5tMPAuC.js → ganttDiagram-NO4QXBWP-AuETTA_E.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-DCRbi4KE.js → gitGraphDiagram-IHSO6WYX-gTS7mo_B.js} +1 -1
- package/web/dist/assets/{index-CO-D6M4t.js → index-29eY0ERe.js} +2 -2
- package/web/dist/assets/index-D4OiytLj.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-Bex5pSIX.js → infoDiagram-FWYZ7A6U-Fhv6oMjM.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-Dj0li_TY.js → ishikawaDiagram-FXEZZL3T-BIWy0CgN.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-BtEGEeVr.js → journeyDiagram-5HDEW3XC-0XQ9xC-A.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-BTFfZvYt.js → kanban-definition-HUTT4EX6-ChyhUUfr.js} +1 -1
- package/web/dist/assets/{linear-C3SQCsip.js → linear-CFXBfAtq.js} +1 -1
- package/web/dist/assets/{mermaid.core-DALDJDu8.js → mermaid.core-BPXpLhrQ.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BZf3uw0u.js → mindmap-definition-LN4V7U3C-HEv8CBmG.js} +1 -1
- package/web/dist/assets/{papaparse.min-Bh9MZ-fk.js → papaparse.min-zNHCLHbr.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-PuOleiMg.js → pegDiagram-2B236MQR-Ciaic9kL.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-CcFORUaW.js → pieDiagram-ENE6RG2P-CPBwQT7c.js} +1 -1
- package/web/dist/assets/pilotswarm-t7ZSHp3x.js +109 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CEm0mp4y.js → quadrantDiagram-ABIIQ3AL-Bn1GbLE3.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-DeH-uX0_.js → railroadDiagram-RFXS5EU6-DgbTuh6h.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-CMbJVMc1.js → requirementDiagram-TGXJPOKE-C25kl_pE.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-C9gL5w_b.js → sankeyDiagram-HTMAVEWB-IXSFhGpB.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-BUo8TT74.js → sequenceDiagram-DBY2YBRQ-BkLHDo50.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-C5lqG0ei.js → sizeCapture-X5ZJPWSS-DYnL5G7X.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-DyeISrEU.js → stateDiagram-2N3HPSRC-Cyn7hAE7.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CrXU-8_S.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-DawuBk_a.js → swimlanes-5IMT3BWC-CZ6EgHci.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-QcV2Vg4L.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-BZJ2YJcm.js → timeline-definition-FHXFAJF6-uvqzr_O1.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BMXBVCfe.js → vennDiagram-L72KCM5P-1ca2fSuA.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-DWedKsrQ.js → wardleyDiagram-EHGQE667-DLDKHVSh.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG--XdfYceC.js → xychartDiagram-FW5EYKEG-BcU1gt-_.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/channel-CcOa1LtY.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B2VhHRSV.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B2VhHRSV.js +0 -1
- package/web/dist/assets/index-uhc851H3.css +0 -1
- package/web/dist/assets/pilotswarm-B9rLtXpI.js +0 -109
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-5FZbaloZ.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DL8PgtmM.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.67",
|
|
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.67",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
@@ -2530,13 +2530,16 @@ export class PilotSwarmUiController {
|
|
|
2530
2530
|
}
|
|
2531
2531
|
await this.refreshSessions();
|
|
2532
2532
|
this.catalogTimer = setInterval(() => {
|
|
2533
|
+
// Do not queue more catalog traffic behind a slow periodic read.
|
|
2534
|
+
if (this.catalogPollInFlight) return;
|
|
2535
|
+
this.catalogPollInFlight = true;
|
|
2533
2536
|
this.refreshSessions().catch((error) => {
|
|
2534
2537
|
this.dispatch({
|
|
2535
2538
|
type: "connection/error",
|
|
2536
2539
|
error: error?.message || String(error),
|
|
2537
2540
|
statusText: SESSION_REFRESH_FAILED_STATUS,
|
|
2538
2541
|
});
|
|
2539
|
-
});
|
|
2542
|
+
}).finally(() => { this.catalogPollInFlight = false; });
|
|
2540
2543
|
}, 4000);
|
|
2541
2544
|
}
|
|
2542
2545
|
|
|
@@ -2628,6 +2631,16 @@ export class PilotSwarmUiController {
|
|
|
2628
2631
|
const recoveringConnection = !preRefreshState.connection.connected || Boolean(preRefreshState.connection.error);
|
|
2629
2632
|
const shouldClearRefreshFailureBanner = preRefreshState.ui.statusText === SESSION_REFRESH_FAILED_STATUS;
|
|
2630
2633
|
const previousActive = this.getState().sessions.activeSessionId;
|
|
2634
|
+
const previousNavigationGeneration = this.navigationGeneration;
|
|
2635
|
+
// The folder catalog is independent of the session pages: overlap its RTT.
|
|
2636
|
+
const groupRowsPromise = typeof this.transport.listSessionGroups === "function"
|
|
2637
|
+
? Promise.resolve().then(() => this.transport.listSessionGroups())
|
|
2638
|
+
.then(groups => (Array.isArray(groups) ? groups : []).map(sessionGroupToRow).filter(Boolean))
|
|
2639
|
+
.catch(error => {
|
|
2640
|
+
console.warn(`[PilotSwarmUi] session-group fetch failed, keeping the known folders: ${error?.message || error}`);
|
|
2641
|
+
return null;
|
|
2642
|
+
})
|
|
2643
|
+
: Promise.resolve(null);
|
|
2631
2644
|
let sessions = (await loadSessionCatalogPageWindow(this.transport)).map(normalizeSessionListRow);
|
|
2632
2645
|
// Folders are NOT merged into the session payload any more: they live
|
|
2633
2646
|
// in their own state slice, so a session refresh cannot drop them. A
|
|
@@ -2641,15 +2654,7 @@ export class PilotSwarmUiController {
|
|
|
2641
2654
|
// state alone is judge which folders are still claimed, since the store
|
|
2642
2655
|
// still holds the PREVIOUS membership at this point; the incoming rows
|
|
2643
2656
|
// ride along on the action for exactly that.
|
|
2644
|
-
|
|
2645
|
-
if (typeof this.transport.listSessionGroups === "function") {
|
|
2646
|
-
try {
|
|
2647
|
-
const groups = await this.transport.listSessionGroups();
|
|
2648
|
-
pendingGroupRows = (Array.isArray(groups) ? groups : []).map(sessionGroupToRow).filter(Boolean);
|
|
2649
|
-
} catch (error) {
|
|
2650
|
-
console.warn(`[PilotSwarmUi] session-group fetch failed, keeping the known folders: ${error?.message || error}`);
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2657
|
+
const pendingGroupRows = await groupRowsPromise;
|
|
2653
2658
|
// A pending deep-link target may be readable but absent from the
|
|
2654
2659
|
// paged catalog window — fetch it explicitly. A definitive failure
|
|
2655
2660
|
// (unknown or unshared: identical 404 shapes) fails the intent; the
|
|
@@ -2753,6 +2758,8 @@ export class PilotSwarmUiController {
|
|
|
2753
2758
|
if (!existingHistory?.events?.length) {
|
|
2754
2759
|
await this.ensureSessionHistory(selected, { force: true }).catch(() => {});
|
|
2755
2760
|
}
|
|
2761
|
+
if (this.getState().sessions.activeSessionId !== selected
|
|
2762
|
+
|| this.navigationGeneration !== previousNavigationGeneration) return;
|
|
2756
2763
|
// Boot-restore can hydrate the selection through THIS branch
|
|
2757
2764
|
// instead of loadSession (whether the profile poll beats the first
|
|
2758
2765
|
// sessions refresh is a race), so the canvas snapshot must be
|
|
@@ -2954,7 +2961,27 @@ export class PilotSwarmUiController {
|
|
|
2954
2961
|
return history;
|
|
2955
2962
|
}
|
|
2956
2963
|
|
|
2957
|
-
async ensureSessionHistory(sessionId,
|
|
2964
|
+
async ensureSessionHistory(sessionId, options = {}) {
|
|
2965
|
+
if (!sessionId) return null;
|
|
2966
|
+
if (this.sessionHistoryLoads.has(sessionId)) return this.sessionHistoryLoads.get(sessionId);
|
|
2967
|
+
const existing = this.getState().history.bySessionId.get(sessionId);
|
|
2968
|
+
if (!options.force && existing?.events && existing?.loadState !== "failed") return existing;
|
|
2969
|
+
this.dispatch({ type: "history/loadState", sessionId, loadState: "loading" });
|
|
2970
|
+
const pending = this.loadSessionHistory(sessionId, { ...options, force: true });
|
|
2971
|
+
this.sessionHistoryLoads.set(sessionId, pending);
|
|
2972
|
+
try {
|
|
2973
|
+
const result = await pending;
|
|
2974
|
+
this.dispatch({ type: "history/loadState", sessionId, loadState: "loaded" });
|
|
2975
|
+
return result;
|
|
2976
|
+
} catch (error) {
|
|
2977
|
+
this.dispatch({ type: "history/loadState", sessionId, loadState: "failed" });
|
|
2978
|
+
throw error;
|
|
2979
|
+
} finally {
|
|
2980
|
+
if (this.sessionHistoryLoads.get(sessionId) === pending) this.sessionHistoryLoads.delete(sessionId);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
async loadSessionHistory(sessionId, { force = false } = {}) {
|
|
2958
2985
|
if (!sessionId) return null;
|
|
2959
2986
|
const existingHistory = this.getState().history.bySessionId.get(sessionId);
|
|
2960
2987
|
const requestedLimit = Math.min(
|
|
@@ -2967,9 +2994,6 @@ export class PilotSwarmUiController {
|
|
|
2967
2994
|
if (!force && existingHistory?.events) {
|
|
2968
2995
|
return existingHistory;
|
|
2969
2996
|
}
|
|
2970
|
-
if (!force && this.sessionHistoryLoads.has(sessionId)) {
|
|
2971
|
-
return this.sessionHistoryLoads.get(sessionId);
|
|
2972
|
-
}
|
|
2973
2997
|
|
|
2974
2998
|
// Re-entry catch-up: when the expanded window is already in memory,
|
|
2975
2999
|
// fetch only the delta after lastSeq and append — the user's pulled-in
|
|
@@ -3052,12 +3076,7 @@ export class PilotSwarmUiController {
|
|
|
3052
3076
|
});
|
|
3053
3077
|
}
|
|
3054
3078
|
return history;
|
|
3055
|
-
})()
|
|
3056
|
-
.finally(() => {
|
|
3057
|
-
this.sessionHistoryLoads.delete(sessionId);
|
|
3058
|
-
});
|
|
3059
|
-
|
|
3060
|
-
this.sessionHistoryLoads.set(sessionId, loadPromise);
|
|
3079
|
+
})();
|
|
3061
3080
|
return loadPromise;
|
|
3062
3081
|
}
|
|
3063
3082
|
|
|
@@ -5727,6 +5746,7 @@ export class PilotSwarmUiController {
|
|
|
5727
5746
|
|
|
5728
5747
|
async loadSession(sessionId) {
|
|
5729
5748
|
if (!sessionId) return;
|
|
5749
|
+
const navigationGeneration = this.navigationGeneration = (this.navigationGeneration || 0) + 1;
|
|
5730
5750
|
const active = this.getState().sessions.activeSessionId;
|
|
5731
5751
|
if (active !== sessionId) {
|
|
5732
5752
|
if (this.getState().ui.promptEdit) {
|
|
@@ -5742,10 +5762,13 @@ export class PilotSwarmUiController {
|
|
|
5742
5762
|
this.detachActiveSession();
|
|
5743
5763
|
return;
|
|
5744
5764
|
}
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5765
|
+
// Independent reads share one network-latency window. Cached chat stays visible.
|
|
5766
|
+
await Promise.all([
|
|
5767
|
+
this.ensureSessionHistory(sessionId, { force: true }),
|
|
5768
|
+
this.syncSessionDetail(sessionId).catch(() => {}),
|
|
5769
|
+
]);
|
|
5770
|
+
if (this.navigationGeneration !== navigationGeneration
|
|
5771
|
+
|| this.getState().sessions.activeSessionId !== sessionId) return;
|
|
5749
5772
|
this.attachActiveSession(sessionId);
|
|
5750
5773
|
this.ensureInspectorData().catch(() => {});
|
|
5751
5774
|
// Canvas snapshot rides the selection burst. Invalidate-then-fetch on
|
package/ui/core/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./layout.js";
|
|
|
6
6
|
export * from "./repo-links.js";
|
|
7
7
|
export * from "./repo-import.js";
|
|
8
8
|
export * from "./session-tree.js";
|
|
9
|
+
export * from "./session-search.js";
|
|
9
10
|
export * from "./state.js";
|
|
10
11
|
export * from "./reducer.js";
|
|
11
12
|
export * from "./portal-links.js";
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -1073,8 +1073,11 @@ function baseReducer(state, action) {
|
|
|
1073
1073
|
state.sessions.navigationIntent
|
|
1074
1074
|
&& state.sessions.navigationIntent.status !== "failed",
|
|
1075
1075
|
);
|
|
1076
|
+
// Saved selection is a boot preference. Once this tab navigates,
|
|
1077
|
+
// a slow profile read (or another device) must not navigate it back.
|
|
1076
1078
|
const hasActive = Object.prototype.hasOwnProperty.call(settings, "activeSessionId")
|
|
1077
|
-
&& !navigationIntentLatched
|
|
1079
|
+
&& !navigationIntentLatched
|
|
1080
|
+
&& !state.sessions.localSelectionMade;
|
|
1078
1081
|
const hasLoadedSessions = Object.keys(state.sessions.byId || {}).length > 0;
|
|
1079
1082
|
const nextLayout = hasLayout
|
|
1080
1083
|
? {
|
|
@@ -1385,11 +1388,12 @@ function baseReducer(state, action) {
|
|
|
1385
1388
|
filterExceptionId: null,
|
|
1386
1389
|
ownerFilterAutoAdmitted: null,
|
|
1387
1390
|
};
|
|
1388
|
-
const selection = applyVisibleSessionSelection(state, nextSessions);
|
|
1389
1391
|
return {
|
|
1390
1392
|
...state,
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
+
// Search filters the list, not the attached chat. Keeping
|
|
1394
|
+
// the active session stable also prevents a keystroke in
|
|
1395
|
+
// the search box from moving the list and main pane.
|
|
1396
|
+
sessions: nextSessions,
|
|
1393
1397
|
};
|
|
1394
1398
|
}
|
|
1395
1399
|
|
|
@@ -1888,7 +1892,7 @@ function baseReducer(state, action) {
|
|
|
1888
1892
|
}
|
|
1889
1893
|
|
|
1890
1894
|
case "sessions/selected": {
|
|
1891
|
-
state = { ...state, sessions: { ...state.sessions, listDeselected: false } };
|
|
1895
|
+
state = { ...state, sessions: { ...state.sessions, listDeselected: false, localSelectionMade: true } };
|
|
1892
1896
|
// Per-session chat scroll memory: stash both the outgoing offset
|
|
1893
1897
|
// and whether it follows the bottom. A paused reading position
|
|
1894
1898
|
// stays paused as new messages arrive and across session switches.
|
|
@@ -2191,6 +2195,17 @@ function baseReducer(state, action) {
|
|
|
2191
2195
|
};
|
|
2192
2196
|
}
|
|
2193
2197
|
|
|
2198
|
+
case "history/loadState": {
|
|
2199
|
+
// A gone-session response may already have evicted this entry.
|
|
2200
|
+
if (!state.sessions.byId[action.sessionId]) return state;
|
|
2201
|
+
const bySessionId = cloneHistoryMap(state.history.bySessionId);
|
|
2202
|
+
bySessionId.set(action.sessionId, {
|
|
2203
|
+
...bySessionId.get(action.sessionId),
|
|
2204
|
+
loadState: action.loadState,
|
|
2205
|
+
});
|
|
2206
|
+
return { ...state, history: { ...state.history, bySessionId } };
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2194
2209
|
case "history/set": {
|
|
2195
2210
|
const previousHistory = state.history.bySessionId.get(action.sessionId) || null;
|
|
2196
2211
|
const previousChat = previousHistory?.chat || [];
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
|
|
2
2
|
import { chatCallLine, firstCallLine } from "./chat-activity.js";
|
|
3
3
|
import { canvasKey as canvasSlotKey, parseCanvasKey } from "./state.js";
|
|
4
|
-
import { isManuallyOrderableSession } from "./session-tree.js";
|
|
4
|
+
import { buildSessionTree, isManuallyOrderableSession } from "./session-tree.js";
|
|
5
|
+
import {
|
|
6
|
+
buildSessionSearchDocument,
|
|
7
|
+
parseSessionSearchQuery,
|
|
8
|
+
scoreSessionSearchDocument,
|
|
9
|
+
} from "./session-search.js";
|
|
5
10
|
import {
|
|
6
11
|
createSplashCard,
|
|
7
12
|
isRehydrationNoticeText,
|
|
@@ -1287,6 +1292,132 @@ function matchesSearchQuery(value, query) {
|
|
|
1287
1292
|
return String(value || "").toLowerCase().includes(normalizedQuery);
|
|
1288
1293
|
}
|
|
1289
1294
|
|
|
1295
|
+
const searchedSessionFlatCache = new WeakMap();
|
|
1296
|
+
|
|
1297
|
+
function sessionGroupTitle(session, byId) {
|
|
1298
|
+
if (session?.isGroup) return session.title || "";
|
|
1299
|
+
if (session?.groupId) return byId?.[`group:${session.groupId}`]?.title || "";
|
|
1300
|
+
let current = session;
|
|
1301
|
+
const seen = new Set();
|
|
1302
|
+
while (current?.parentSessionId && !seen.has(current.parentSessionId)) {
|
|
1303
|
+
seen.add(current.parentSessionId);
|
|
1304
|
+
current = byId?.[current.parentSessionId];
|
|
1305
|
+
if (current?.groupId) return byId?.[`group:${current.groupId}`]?.title || "";
|
|
1306
|
+
}
|
|
1307
|
+
return "";
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function sessionSearchOwner(session, byId) {
|
|
1311
|
+
const direct = effectiveSessionOwner(session, byId);
|
|
1312
|
+
if (direct) return direct;
|
|
1313
|
+
let current = session;
|
|
1314
|
+
const seen = new Set();
|
|
1315
|
+
while (current?.parentSessionId && !seen.has(current.parentSessionId)) {
|
|
1316
|
+
seen.add(current.parentSessionId);
|
|
1317
|
+
current = byId?.[current.parentSessionId];
|
|
1318
|
+
if (current?.owner) return current.owner;
|
|
1319
|
+
}
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function buildSearchedSessionFlat(state, query) {
|
|
1324
|
+
const existingFlat = Array.isArray(state.sessions?.flat) ? state.sessions.flat : [];
|
|
1325
|
+
if (!query) return existingFlat;
|
|
1326
|
+
const cached = searchedSessionFlatCache.get(existingFlat);
|
|
1327
|
+
if (cached?.query === query) return cached.result;
|
|
1328
|
+
|
|
1329
|
+
const byId = state.sessions?.byId || {};
|
|
1330
|
+
const parsed = parseSessionSearchQuery(query);
|
|
1331
|
+
const expanded = buildSessionTree(
|
|
1332
|
+
Object.values(byId),
|
|
1333
|
+
new Set(),
|
|
1334
|
+
state.sessions?.orderById,
|
|
1335
|
+
state.sessions?.pinnedIds,
|
|
1336
|
+
state.sessions?.manualOrder,
|
|
1337
|
+
);
|
|
1338
|
+
const parentById = new Map();
|
|
1339
|
+
const entryById = new Map();
|
|
1340
|
+
const baseIndex = new Map();
|
|
1341
|
+
const stack = [];
|
|
1342
|
+
for (let index = 0; index < expanded.length; index += 1) {
|
|
1343
|
+
const entry = expanded[index];
|
|
1344
|
+
stack[entry.depth] = entry.sessionId;
|
|
1345
|
+
stack.length = entry.depth + 1;
|
|
1346
|
+
parentById.set(entry.sessionId, entry.depth > 0 ? stack[entry.depth - 1] : null);
|
|
1347
|
+
entryById.set(entry.sessionId, entry);
|
|
1348
|
+
baseIndex.set(entry.sessionId, index);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const directScores = new Map();
|
|
1352
|
+
for (const entry of expanded) {
|
|
1353
|
+
const session = byId[entry.sessionId] || entry.standIn;
|
|
1354
|
+
if (!session || !matchesOwnerFilter(session, state.sessions?.ownerFilter, state.auth || {}, byId)) continue;
|
|
1355
|
+
const owner = sessionSearchOwner(session, byId);
|
|
1356
|
+
const document = buildSessionSearchDocument(session, {
|
|
1357
|
+
owner,
|
|
1358
|
+
groupTitle: sessionGroupTitle(session, byId),
|
|
1359
|
+
});
|
|
1360
|
+
const score = scoreSessionSearchDocument(document, parsed);
|
|
1361
|
+
if (score > 0) directScores.set(entry.sessionId, score);
|
|
1362
|
+
}
|
|
1363
|
+
const matchedIds = new Set(directScores.keys());
|
|
1364
|
+
// A just-created or deep-linked session may be admitted explicitly while
|
|
1365
|
+
// the user's prior query still excludes it. Preserve that established
|
|
1366
|
+
// visibility contract and rank the exception first until the user edits
|
|
1367
|
+
// the search (which clears filterExceptionId in the reducer).
|
|
1368
|
+
const exceptionId = state.sessions?.filterExceptionId;
|
|
1369
|
+
if (exceptionId && entryById.has(exceptionId)) {
|
|
1370
|
+
directScores.set(exceptionId, Number.MAX_SAFE_INTEGER);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const included = new Set();
|
|
1374
|
+
const subtreeScores = new Map();
|
|
1375
|
+
for (const [sessionId, score] of directScores) {
|
|
1376
|
+
let current = sessionId;
|
|
1377
|
+
const seen = new Set();
|
|
1378
|
+
while (current && !seen.has(current)) {
|
|
1379
|
+
seen.add(current);
|
|
1380
|
+
included.add(current);
|
|
1381
|
+
subtreeScores.set(current, Math.max(subtreeScores.get(current) || 0, score));
|
|
1382
|
+
current = parentById.get(current) || null;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const children = new Map();
|
|
1387
|
+
for (const sessionId of included) {
|
|
1388
|
+
const parentId = parentById.get(sessionId) || null;
|
|
1389
|
+
if (!children.has(parentId)) children.set(parentId, []);
|
|
1390
|
+
children.get(parentId).push(sessionId);
|
|
1391
|
+
}
|
|
1392
|
+
for (const siblings of children.values()) {
|
|
1393
|
+
siblings.sort((a, b) => (subtreeScores.get(b) || 0) - (subtreeScores.get(a) || 0)
|
|
1394
|
+
|| (baseIndex.get(a) || 0) - (baseIndex.get(b) || 0));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const result = [];
|
|
1398
|
+
const visit = (sessionId, depth) => {
|
|
1399
|
+
const entry = entryById.get(sessionId);
|
|
1400
|
+
if (!entry) return;
|
|
1401
|
+
const childIds = children.get(sessionId) || [];
|
|
1402
|
+
result.push({
|
|
1403
|
+
...entry,
|
|
1404
|
+
depth,
|
|
1405
|
+
collapsed: false,
|
|
1406
|
+
hasChildren: childIds.length > 0,
|
|
1407
|
+
searchIncluded: true,
|
|
1408
|
+
searchMatch: matchedIds.has(sessionId),
|
|
1409
|
+
searchScore: directScores.get(sessionId) || 0,
|
|
1410
|
+
});
|
|
1411
|
+
for (const childId of childIds) visit(childId, depth + 1);
|
|
1412
|
+
};
|
|
1413
|
+
for (const rootId of children.get(null) || []) visit(rootId, 0);
|
|
1414
|
+
// A user can type an unbounded number of distinct queries while the
|
|
1415
|
+
// catalog array keeps the same identity. Keep only the latest result for
|
|
1416
|
+
// that catalog rather than retaining every intermediate keystroke.
|
|
1417
|
+
searchedSessionFlatCache.set(existingFlat, { query, result });
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1290
1421
|
// The transient deep-link filter exception covers the linked session AND its
|
|
1291
1422
|
// ancestor chain (real parents plus the synthetic group:<id> row) so the
|
|
1292
1423
|
// linked row renders with its tree context instead of as an orphan.
|
|
@@ -1330,22 +1461,25 @@ function sameRowDeps(a, b) {
|
|
|
1330
1461
|
}
|
|
1331
1462
|
|
|
1332
1463
|
export function selectSessionRows(state) {
|
|
1464
|
+
// Keep field punctuation and quotes for the parser (`author:"Ada Lovelace"`).
|
|
1465
|
+
// Individual values are normalized inside session-search.
|
|
1466
|
+
const query = String(state.sessions?.filterQuery || "").trim();
|
|
1467
|
+
const sessionFlat = buildSearchedSessionFlat(state, query);
|
|
1333
1468
|
const totalDescendantCounts = getTotalDescendantCounts(state.sessions.byId);
|
|
1334
|
-
const visibleDescendantCounts = getVisibleDescendantCounts(
|
|
1335
|
-
const query = normalizeSearchQuery(state.sessions?.filterQuery || "");
|
|
1469
|
+
const visibleDescendantCounts = getVisibleDescendantCounts(sessionFlat, state.sessions.byId);
|
|
1336
1470
|
const ownerFilter = state.sessions?.ownerFilter || { all: true };
|
|
1337
1471
|
const auth = state.auth || {};
|
|
1338
1472
|
const pinnedSet = new Set(Array.isArray(state.sessions?.pinnedIds) ? state.sessions.pinnedIds : []);
|
|
1339
1473
|
const selectedSet = new Set(Array.isArray(state.sessions?.selectedIds) ? state.sessions.selectedIds : []);
|
|
1340
1474
|
const filterExceptionIds = collectFilterExceptionIds(state.sessions);
|
|
1341
1475
|
|
|
1342
|
-
let memo = sessionRowMemo.get(
|
|
1476
|
+
let memo = sessionRowMemo.get(sessionFlat);
|
|
1343
1477
|
if (!memo) {
|
|
1344
1478
|
memo = new Map();
|
|
1345
|
-
sessionRowMemo.set(
|
|
1479
|
+
sessionRowMemo.set(sessionFlat, memo);
|
|
1346
1480
|
}
|
|
1347
1481
|
|
|
1348
|
-
return
|
|
1482
|
+
return sessionFlat.filter((entry) => (
|
|
1349
1483
|
// A row whose session is in neither place can render nothing but an
|
|
1350
1484
|
// owner chip and a dash - the "[?]" ghost. Drop it rather than show it.
|
|
1351
1485
|
Boolean(state.sessions.byId[entry.sessionId] || entry.standIn)
|
|
@@ -1433,10 +1567,13 @@ export function selectSessionRows(state) {
|
|
|
1433
1567
|
// `chrome` as well so a host that draws its own row (the phone's
|
|
1434
1568
|
// list) can act on it without reading the portal's descriptor.
|
|
1435
1569
|
pause: rowView.chrome?.pause || null,
|
|
1570
|
+
searchMatch: entry.searchMatch === true,
|
|
1571
|
+
searchScore: Number(entry.searchScore) || 0,
|
|
1436
1572
|
};
|
|
1437
1573
|
memo.set(entry.sessionId, { deps, row });
|
|
1438
1574
|
return row;
|
|
1439
1575
|
}).filter((row) => {
|
|
1576
|
+
if (query) return true; // searchedSessionFlat already contains only matches and their ancestors.
|
|
1440
1577
|
if (filterExceptionIds?.has(row.sessionId)) return true;
|
|
1441
1578
|
const session = state.sessions.byId[row.sessionId];
|
|
1442
1579
|
if (!matchesOwnerFilter(session, ownerFilter, auth, state.sessions.byId)) return false;
|
|
@@ -1444,11 +1581,9 @@ export function selectSessionRows(state) {
|
|
|
1444
1581
|
const ownerSearchText = effectiveOwner
|
|
1445
1582
|
? `${ownerDisplayName(effectiveOwner, "")} ${effectiveOwner.email || ""}`
|
|
1446
1583
|
: "";
|
|
1447
|
-
const summarySearchText = "";
|
|
1448
1584
|
return matchesSearchQuery(row.text, query)
|
|
1449
1585
|
|| matchesSearchQuery(row.sessionId, query)
|
|
1450
|
-
|| matchesSearchQuery(ownerSearchText, query)
|
|
1451
|
-
|| matchesSearchQuery(summarySearchText, query);
|
|
1586
|
+
|| matchesSearchQuery(ownerSearchText, query);
|
|
1452
1587
|
});
|
|
1453
1588
|
}
|
|
1454
1589
|
|
|
@@ -1893,6 +2028,16 @@ export function selectActiveChat(state) {
|
|
|
1893
2028
|
}
|
|
1894
2029
|
const history = state.history.bySessionId.get(sessionId);
|
|
1895
2030
|
const chat = history?.chat || [];
|
|
2031
|
+
if (!chat.length && ["loading", "failed"].includes(history?.loadState)) {
|
|
2032
|
+
return [{
|
|
2033
|
+
id: `history-load:${sessionId}:${history.loadState}`,
|
|
2034
|
+
role: "system",
|
|
2035
|
+
noChrome: true,
|
|
2036
|
+
text: history.loadState === "loading"
|
|
2037
|
+
? "Loading conversation…"
|
|
2038
|
+
: "Could not load this conversation. Select the session again to retry.",
|
|
2039
|
+
}];
|
|
2040
|
+
}
|
|
1896
2041
|
const events = history?.events || [];
|
|
1897
2042
|
const pendingQuestionMessage = session?.pendingQuestion?.question
|
|
1898
2043
|
&& !chatAlreadyContainsPendingQuestion(chat, session.pendingQuestion.question)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const FIELD_ALIASES = Object.freeze({
|
|
2
|
+
author: "author",
|
|
3
|
+
owner: "author",
|
|
4
|
+
agent: "agent",
|
|
5
|
+
status: "status",
|
|
6
|
+
group: "group",
|
|
7
|
+
folder: "group",
|
|
8
|
+
model: "model",
|
|
9
|
+
id: "id",
|
|
10
|
+
topic: "topic",
|
|
11
|
+
summary: "summary",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const FREE_FIELD_WEIGHTS = Object.freeze({
|
|
15
|
+
title: 12,
|
|
16
|
+
id: 11,
|
|
17
|
+
author: 10,
|
|
18
|
+
agent: 9,
|
|
19
|
+
summary: 7,
|
|
20
|
+
group: 6,
|
|
21
|
+
model: 5,
|
|
22
|
+
status: 4,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function normalizeSessionSearchText(value) {
|
|
26
|
+
return String(value || "")
|
|
27
|
+
.normalize("NFKD")
|
|
28
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9@._+-]+/g, " ")
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/\s+/g, " ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseSessionSearchQuery(value) {
|
|
36
|
+
const source = String(value || "").trim();
|
|
37
|
+
const terms = [];
|
|
38
|
+
const filters = [];
|
|
39
|
+
const pattern = /(?:(\w+):)?(?:"([^"]+)"|(\S+))/g;
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
42
|
+
const rawField = String(match[1] || "").toLowerCase();
|
|
43
|
+
const rawValue = match[2] ?? match[3] ?? "";
|
|
44
|
+
const normalized = normalizeSessionSearchText(rawValue);
|
|
45
|
+
if (!normalized) continue;
|
|
46
|
+
const field = FIELD_ALIASES[rawField];
|
|
47
|
+
if (field) filters.push({ field, value: normalized });
|
|
48
|
+
else terms.push(normalizeSessionSearchText(rawField ? `${rawField} ${rawValue}` : rawValue));
|
|
49
|
+
}
|
|
50
|
+
return { source, terms: terms.filter(Boolean), filters };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function boundedDistance(a, b, limit) {
|
|
54
|
+
if (Math.abs(a.length - b.length) > limit) return limit + 1;
|
|
55
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
56
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
57
|
+
const current = [i];
|
|
58
|
+
let rowMinimum = current[0];
|
|
59
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
60
|
+
const value = Math.min(
|
|
61
|
+
current[j - 1] + 1,
|
|
62
|
+
previous[j] + 1,
|
|
63
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
64
|
+
);
|
|
65
|
+
current.push(value);
|
|
66
|
+
rowMinimum = Math.min(rowMinimum, value);
|
|
67
|
+
}
|
|
68
|
+
if (rowMinimum > limit) return limit + 1;
|
|
69
|
+
previous = current;
|
|
70
|
+
}
|
|
71
|
+
return previous[b.length];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchStrength(haystack, needle) {
|
|
75
|
+
if (!haystack || !needle) return 0;
|
|
76
|
+
if (haystack === needle) return 16;
|
|
77
|
+
const words = haystack.split(" ");
|
|
78
|
+
if (words.includes(needle)) return 14;
|
|
79
|
+
if (haystack.includes(needle)) return needle.includes(" ") ? 13 : 11;
|
|
80
|
+
if (needle.length >= 2 && words.some((word) => word.startsWith(needle))) return 9;
|
|
81
|
+
if (needle.length >= 4 && words.some((word) => word.includes(needle))) return 7;
|
|
82
|
+
if (needle.length < 4) return 0;
|
|
83
|
+
// One adjacent transposition is two edits under Levenshtein distance
|
|
84
|
+
// ("session" → "sessoin"). Permit that for ordinary words while keeping
|
|
85
|
+
// short tokens exact/prefix-only.
|
|
86
|
+
const limit = needle.length >= 6 ? 2 : 1;
|
|
87
|
+
return words.some((word) => boundedDistance(word, needle, limit) <= limit) ? 4 : 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summaryStrings(summaryState) {
|
|
91
|
+
if (!summaryState || typeof summaryState !== "object") return [];
|
|
92
|
+
return [summaryState.topic, summaryState.intent, summaryState.summary, summaryState.title]
|
|
93
|
+
.filter((value) => typeof value === "string" && value.trim());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildSessionSearchDocument(session, { owner = null, groupTitle = "" } = {}) {
|
|
97
|
+
const title = normalizeSessionSearchText(session?.title);
|
|
98
|
+
const summary = normalizeSessionSearchText([
|
|
99
|
+
session?.shortSummary,
|
|
100
|
+
...summaryStrings(session?.summaryState),
|
|
101
|
+
].filter(Boolean).join(" "));
|
|
102
|
+
const author = normalizeSessionSearchText([
|
|
103
|
+
owner?.displayName,
|
|
104
|
+
owner?.email,
|
|
105
|
+
owner?.subject,
|
|
106
|
+
].filter(Boolean).join(" "));
|
|
107
|
+
return {
|
|
108
|
+
title,
|
|
109
|
+
summary,
|
|
110
|
+
topic: normalizeSessionSearchText(`${title} ${summary}`),
|
|
111
|
+
author,
|
|
112
|
+
agent: normalizeSessionSearchText(session?.agentId),
|
|
113
|
+
status: normalizeSessionSearchText(session?.status),
|
|
114
|
+
group: normalizeSessionSearchText(groupTitle),
|
|
115
|
+
model: normalizeSessionSearchText(`${session?.model || ""} ${session?.reasoningEffort || ""}`),
|
|
116
|
+
id: normalizeSessionSearchText(session?.sessionId),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Score one already-authorized, browser-resident session. Structured filters
|
|
122
|
+
* and free-text terms are ANDed; each free term may match any field. Exact,
|
|
123
|
+
* prefix and substring matches beat bounded typo matches.
|
|
124
|
+
*/
|
|
125
|
+
export function scoreSessionSearchDocument(document, query) {
|
|
126
|
+
const parsed = typeof query === "string" ? parseSessionSearchQuery(query) : query;
|
|
127
|
+
if (!parsed || (parsed.terms.length === 0 && parsed.filters.length === 0)) return 1;
|
|
128
|
+
let score = 0;
|
|
129
|
+
for (const filter of parsed.filters) {
|
|
130
|
+
const strength = matchStrength(document?.[filter.field] || "", filter.value);
|
|
131
|
+
if (!strength) return 0;
|
|
132
|
+
score += strength * 14;
|
|
133
|
+
}
|
|
134
|
+
for (const term of parsed.terms) {
|
|
135
|
+
let best = 0;
|
|
136
|
+
for (const [field, weight] of Object.entries(FREE_FIELD_WEIGHTS)) {
|
|
137
|
+
// Short numbers such as "Session 2" occur in the fixed portions
|
|
138
|
+
// of every UUID. Treat IDs as an explicit/long-fragment field so
|
|
139
|
+
// the human title remains the natural interpretation; `id:...`
|
|
140
|
+
// is always available for deliberate ID lookup.
|
|
141
|
+
if (field === "id" && (/^\d+$/.test(term) || term.length < 8)) continue;
|
|
142
|
+
best = Math.max(best, matchStrength(document?.[field] || "", term) * weight);
|
|
143
|
+
}
|
|
144
|
+
if (!best) return 0;
|
|
145
|
+
score += best;
|
|
146
|
+
}
|
|
147
|
+
const phrase = parsed.terms.join(" ");
|
|
148
|
+
if (phrase && document?.title?.includes(phrase)) score += 120;
|
|
149
|
+
else if (phrase && document?.summary?.includes(phrase)) score += 60;
|
|
150
|
+
return score;
|
|
151
|
+
}
|