pilotswarm 0.5.70 → 0.5.72

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.
Files changed (76) hide show
  1. package/package.json +2 -2
  2. package/ui/core/src/controller.js +3 -1
  3. package/ui/core/src/moa.js +2 -2
  4. package/ui/core/src/reducer.js +14 -2
  5. package/ui/core/src/selectors.js +3 -3
  6. package/ui/core/src/session-sort.js +45 -0
  7. package/ui/core/src/state.js +3 -0
  8. package/ui/react/src/web-app.js +145 -49
  9. package/web/dist/assets/{abnfDiagram-VRR7QNED-CidJDJrY.js → abnfDiagram-VRR7QNED-D__RWhK7.js} +1 -1
  10. package/web/dist/assets/{arc-BDO1TDjs.js → arc-CjtcEiTR.js} +1 -1
  11. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-BIcgz3Aa.js → architectureDiagram-ZJ3FMSHR-BDI8FioV.js} +1 -1
  12. package/web/dist/assets/{blockDiagram-677ZJIJ3-CWtdYLva.js → blockDiagram-677ZJIJ3-D62iIbEl.js} +1 -1
  13. package/web/dist/assets/{c4Diagram-LMCZKHZV-u_IOLAS7.js → c4Diagram-LMCZKHZV-BlJ596a2.js} +1 -1
  14. package/web/dist/assets/channel-CF_IY6bX.js +1 -0
  15. package/web/dist/assets/{chunk-2Q5K7J3B-daWW1UxL.js → chunk-2Q5K7J3B-CnVtHjtN.js} +1 -1
  16. package/web/dist/assets/{chunk-32BRIVSS-DyxnM56K.js → chunk-32BRIVSS-g8IU-e70.js} +1 -1
  17. package/web/dist/assets/{chunk-5VM5RSS4-D7taC4Nx.js → chunk-5VM5RSS4-KKxfFv2y.js} +1 -1
  18. package/web/dist/assets/{chunk-EX3LRPZG-BWo08pDw.js → chunk-EX3LRPZG-C0eEpQtc.js} +1 -1
  19. package/web/dist/assets/{chunk-JWPE2WC7-AhysI3TD.js → chunk-JWPE2WC7-ZfmDblU2.js} +1 -1
  20. package/web/dist/assets/{chunk-MOJQB5TN-CDyGPb8L.js → chunk-MOJQB5TN-BOfHzBMe.js} +1 -1
  21. package/web/dist/assets/{chunk-RYQCIY6F-RhKa7iem.js → chunk-RYQCIY6F-GvY7zGfa.js} +1 -1
  22. package/web/dist/assets/{chunk-V7JOEXUC-DLgws19l.js → chunk-V7JOEXUC-Bi28CvqE.js} +1 -1
  23. package/web/dist/assets/{chunk-VR4S4FIN-CyLmmpZk.js → chunk-VR4S4FIN-H1DY3Xw7.js} +1 -1
  24. package/web/dist/assets/{chunk-XXDRQBXY-DYN0KnEU.js → chunk-XXDRQBXY-JMwCi2he.js} +1 -1
  25. package/web/dist/assets/classDiagram-OUVF2IWQ-BoTycRRp.js +1 -0
  26. package/web/dist/assets/classDiagram-v2-EOCWNBFH-BoTycRRp.js +1 -0
  27. package/web/dist/assets/{cose-bilkent-JH36ORCC-62MpINYU.js → cose-bilkent-JH36ORCC-DfFOvDA3.js} +1 -1
  28. package/web/dist/assets/{cynefin-VYW2F7L2-CHVeQsGv.js → cynefin-VYW2F7L2-CD5a_UMt.js} +1 -1
  29. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BYgzn_3A.js → cynefinDiagram-TSTJHNR4-CRsYXGC9.js} +1 -1
  30. package/web/dist/assets/{dagre-VKFMJZFB-C3qJqyHj.js → dagre-VKFMJZFB-BuPdi3xA.js} +1 -1
  31. package/web/dist/assets/{diagram-FQU43EPY-BBYvv2Hx.js → diagram-FQU43EPY-MIVIpIBC.js} +1 -1
  32. package/web/dist/assets/{diagram-G47NLZAW-1Ca1QwqV.js → diagram-G47NLZAW-D7-s_wbT.js} +1 -1
  33. package/web/dist/assets/{diagram-NH7WQ7WH-CicNpuah.js → diagram-NH7WQ7WH-fmSlDjpy.js} +1 -1
  34. package/web/dist/assets/{diagram-OA4YK3LP-BDGMGAu8.js → diagram-OA4YK3LP-DwEwWIum.js} +1 -1
  35. package/web/dist/assets/{diagram-WEI45ONY-EgaBHcZC.js → diagram-WEI45ONY-Gvb_KdCz.js} +1 -1
  36. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-BEGQ-CpA.js → ebnfDiagram-CCIWWBDH-BOWGc2OC.js} +1 -1
  37. package/web/dist/assets/{erDiagram-Q63AITRT-BifquFiz.js → erDiagram-Q63AITRT-CrigFfDY.js} +1 -1
  38. package/web/dist/assets/{flowDiagram-23GEKE2U-BOEV0nau.js → flowDiagram-23GEKE2U-618PQgNe.js} +1 -1
  39. package/web/dist/assets/{ganttDiagram-NO4QXBWP-Die6C1uw.js → ganttDiagram-NO4QXBWP-lHe8_OHE.js} +1 -1
  40. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BS6qOLxN.js → gitGraphDiagram-IHSO6WYX-CM3Jo1NF.js} +1 -1
  41. package/web/dist/assets/index-DHFBzAwY.css +1 -0
  42. package/web/dist/assets/index-G-9aAxKP.js +24 -0
  43. package/web/dist/assets/{infoDiagram-FWYZ7A6U-C3dgkkDc.js → infoDiagram-FWYZ7A6U-CtDt5Yg1.js} +1 -1
  44. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-BmGRDlL-.js → ishikawaDiagram-FXEZZL3T-DMDXe5yy.js} +1 -1
  45. package/web/dist/assets/{journeyDiagram-5HDEW3XC-De0y-2Pp.js → journeyDiagram-5HDEW3XC-BxUjwmwF.js} +1 -1
  46. package/web/dist/assets/{kanban-definition-HUTT4EX6-CPVl9fbH.js → kanban-definition-HUTT4EX6-Cey1IRnt.js} +1 -1
  47. package/web/dist/assets/{linear-BwcW14FN.js → linear-BAaF8u7Q.js} +1 -1
  48. package/web/dist/assets/{mermaid.core-BPC8y2lr.js → mermaid.core-D7ywN4EU.js} +4 -4
  49. package/web/dist/assets/{mindmap-definition-LN4V7U3C-CmTjYZ6-.js → mindmap-definition-LN4V7U3C-BTSESh2V.js} +1 -1
  50. package/web/dist/assets/{papaparse.min-DwUBIiJJ.js → papaparse.min-BhnWt-zK.js} +1 -1
  51. package/web/dist/assets/{pegDiagram-2B236MQR-DI77hWU9.js → pegDiagram-2B236MQR-BTWzwmna.js} +1 -1
  52. package/web/dist/assets/{pieDiagram-ENE6RG2P-CQL62GJk.js → pieDiagram-ENE6RG2P-qI9jL2eV.js} +1 -1
  53. package/web/dist/assets/pilotswarm-Dccv5YRO.js +109 -0
  54. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CQoKIYFZ.js → quadrantDiagram-ABIIQ3AL-B54Va5N3.js} +1 -1
  55. package/web/dist/assets/{railroadDiagram-RFXS5EU6-B2KkUNRk.js → railroadDiagram-RFXS5EU6-CVXDflXo.js} +1 -1
  56. package/web/dist/assets/{requirementDiagram-TGXJPOKE-Dgl152x0.js → requirementDiagram-TGXJPOKE-C7abNeEc.js} +1 -1
  57. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-B-SjWvvt.js → sankeyDiagram-HTMAVEWB-ND6Feq2w.js} +1 -1
  58. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CuuRcXdJ.js → sequenceDiagram-DBY2YBRQ-DXGwvQdf.js} +1 -1
  59. package/web/dist/assets/{sizeCapture-X5ZJPWSS-TzEpZfA7.js → sizeCapture-X5ZJPWSS-CrXKKsBI.js} +1 -1
  60. package/web/dist/assets/{stateDiagram-2N3HPSRC-XAi55Ttk.js → stateDiagram-2N3HPSRC-BJWpN6s6.js} +1 -1
  61. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BQY9l8FM.js +1 -0
  62. package/web/dist/assets/{swimlanes-5IMT3BWC-C6ATJwDO.js → swimlanes-5IMT3BWC-Cy67JyBd.js} +2 -2
  63. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DZ0Jn4Gp.js +8 -0
  64. package/web/dist/assets/{timeline-definition-FHXFAJF6-DhUjhdEH.js → timeline-definition-FHXFAJF6-CeDd4R0u.js} +1 -1
  65. package/web/dist/assets/{vennDiagram-L72KCM5P-CxoeZ7Ri.js → vennDiagram-L72KCM5P-BDrPrK0B.js} +1 -1
  66. package/web/dist/assets/{wardleyDiagram-EHGQE667-ChgUaxkB.js → wardleyDiagram-EHGQE667-Bp9PEqNv.js} +1 -1
  67. package/web/dist/assets/{xychartDiagram-FW5EYKEG-iV0ACrP1.js → xychartDiagram-FW5EYKEG-DI_8u8T1.js} +1 -1
  68. package/web/dist/index.html +3 -3
  69. package/web/dist/assets/channel-KxoOVq0K.js +0 -1
  70. package/web/dist/assets/classDiagram-OUVF2IWQ-B6yEKJ21.js +0 -1
  71. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B6yEKJ21.js +0 -1
  72. package/web/dist/assets/index-CCniDa-9.css +0 -1
  73. package/web/dist/assets/index-DpkfY3jc.js +0 -24
  74. package/web/dist/assets/pilotswarm-BnfVgmru.js +0 -109
  75. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CXVciPM_.js +0 -1
  76. package/web/dist/assets/swimlanesDiagram-G3AALYLV-Dk-VTEgD.js +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm",
3
- "version": "0.5.70",
3
+ "version": "0.5.72",
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.70",
84
+ "pilotswarm-sdk": "0.5.72",
85
85
  "react": "^19.2.4",
86
86
  "react-dom": "^19.2.4",
87
87
  "ws": "^8.18.2"
@@ -7545,6 +7545,7 @@ export class PilotSwarmUiController {
7545
7545
  force = true,
7546
7546
  fullscreen = false,
7547
7547
  pane = false,
7548
+ preserveSession = false,
7548
7549
  } = {}) {
7549
7550
  const resolvedSessionId = sessionId || this.getState().sessions.activeSessionId;
7550
7551
  const name = String(filename || "").trim();
@@ -7558,7 +7559,7 @@ export class PilotSwarmUiController {
7558
7559
  restoreArtifactId: this.getState().files.selectedArtifactId || null,
7559
7560
  });
7560
7561
 
7561
- if (this.getState().sessions.activeSessionId !== resolvedSessionId) {
7562
+ if (!preserveSession && this.getState().sessions.activeSessionId !== resolvedSessionId) {
7562
7563
  await this.loadSession(resolvedSessionId).catch(() => null);
7563
7564
  }
7564
7565
  // The reader REPLACES the inspector, so it has no business changing
@@ -8996,6 +8997,7 @@ export class PilotSwarmUiController {
8996
8997
  return;
8997
8998
  }
8998
8999
 
9000
+ this.dispatch({ type: "sessions/used", sessionId });
8999
9001
  const activePendingQuestion = activeSession?.pendingQuestion || null;
9000
9002
  const answeringPendingQuestion = Boolean(activePendingQuestion?.question);
9001
9003
  const promptEdit = this.getPromptEditSessionMatch(sessionId);
@@ -42,7 +42,7 @@ export function normalizeMoa(value) {
42
42
  return safeLayout(dashboard, key, `MoA ${index + 1}`);
43
43
  });
44
44
  if (!dashboards.length) dashboards.push(safeLayout(null));
45
- return { version: 3, activeDashboardId: dashboards.some(d => d.id === value.activeDashboardId) ? value.activeDashboardId : dashboards[0].id, dashboards };
45
+ return { version: 3, composerMode: value?.composerMode === "shared" ? "shared" : "per-chat", activeDashboardId: dashboards.some(d => d.id === value.activeDashboardId) ? value.activeDashboardId : dashboards[0].id, dashboards };
46
46
  }
47
47
  let source = value;
48
48
  if (!(value?.version === 2 || (object(value) && Object.hasOwn(value, "tree")))) {
@@ -51,7 +51,7 @@ export function normalizeMoa(value) {
51
51
  const active = Number.isInteger(value?.activeSlot) ? Math.max(0, Math.min(4, value.activeSlot)) : 0;
52
52
  source = safeLayout(slots[active]).tree ? slots[active] : slots.find(slot => safeLayout(slot).tree);
53
53
  }
54
- return { version: 3, activeDashboardId: "moa-1", dashboards: [safeLayout(source)] };
54
+ return { version: 3, composerMode: value?.composerMode === "shared" ? "shared" : "per-chat", activeDashboardId: "moa-1", dashboards: [safeLayout(source)] };
55
55
  }
56
56
  export const activeMoaDashboard = value => value.dashboards.find(d => d.id === value.activeDashboardId) || value.dashboards[0];
57
57
  export function updateMoaDashboard(value, dashboardId, patch) {
@@ -1,3 +1,4 @@
1
+ import { normalizeSessionSortMode, normalizeSessionUsage, reconcileSessionSort } from "./session-sort.js";
1
2
  import { normalizeMoa } from "./moa.js";
2
3
  import { retainSessionWarnings } from "./session-errors.js";
3
4
  import { buildSessionTree, isManuallyOrderableSession } from "./session-tree.js";
@@ -846,7 +847,7 @@ export function appReducer(state, action) {
846
847
  if (sessionId && contentUpdate && state.sessions?.goneIds?.includes(sessionId)) return state;
847
848
  const next = baseReducer(state, action);
848
849
  if (next === state) return next;
849
- return reconcileSessionView(state, next, action);
850
+ return reconcileSessionView(state, reconcileSessionSort(state, next, action), action);
850
851
  }
851
852
 
852
853
  // Per-session desktop views, kept in step with the ui slice.
@@ -1101,6 +1102,7 @@ function baseReducer(state, action) {
1101
1102
  const nextActiveSessionId = hasActive
1102
1103
  ? normalizeStoredActiveSessionId(settings.activeSessionId)
1103
1104
  : state.sessions.activeSessionId;
1105
+ const remoteUsage = normalizeSessionUsage(settings.sessionUsedAt);
1104
1106
  const nextSessions = {
1105
1107
  ...state.sessions,
1106
1108
  ...(hasOwnerFilter
@@ -1117,6 +1119,9 @@ function baseReducer(state, action) {
1117
1119
  : {}),
1118
1120
  pinnedIds: nextPinnedIds,
1119
1121
  manualOrder: nextManualOrder,
1122
+ sortMode: Object.hasOwn(settings, "sessionSortMode") ? normalizeSessionSortMode(settings.sessionSortMode) : state.sessions.sortMode,
1123
+ usedAt: Object.fromEntries([...new Set([...Object.keys(remoteUsage), ...Object.keys(state.sessions.usedAt || {})])]
1124
+ .map(id => [id, Math.max(remoteUsage[id] || 0, state.sessions.usedAt?.[id] || 0)])),
1120
1125
  collapsedIds: nextCollapsedIds,
1121
1126
  collapsedIdsExplicit: hasCollapsed ? true : state.sessions.collapsedIdsExplicit,
1122
1127
  activeSessionId: nextActiveSessionId,
@@ -1891,8 +1896,15 @@ function baseReducer(state, action) {
1891
1896
  };
1892
1897
  }
1893
1898
 
1899
+ case "sessions/sortMode":
1900
+ return { ...state, sessions: { ...state.sessions, sortMode: normalizeSessionSortMode(action.mode) } };
1901
+ case "sessions/refreshSort":
1902
+ return { ...state, sessions: { ...state.sessions } };
1903
+ case "sessions/used":
1904
+ if (!action.sessionId) return state;
1905
+ return { ...state, sessions: { ...state.sessions, usedAt: { ...state.sessions.usedAt, [action.sessionId]: action.at || Date.now() } } };
1894
1906
  case "sessions/selected": {
1895
- state = { ...state, sessions: { ...state.sessions, listDeselected: false, localSelectionMade: true } };
1907
+ state = { ...state, sessions: { ...state.sessions, listDeselected: false, localSelectionMade: true, usedAt: action.sessionId ? { ...state.sessions.usedAt, [action.sessionId]: Date.now() } : state.sessions.usedAt } };
1896
1908
  // Per-session chat scroll memory: stash both the outgoing offset
1897
1909
  // and whether it follows the bottom. A paused reading position
1898
1910
  // stays paused as new messages arrive and across session switches.
@@ -1332,7 +1332,7 @@ function sessionSearchOwner(session, byId) {
1332
1332
 
1333
1333
  function sessionSearchIndex(state) {
1334
1334
  const byId = state.sessions?.byId || {};
1335
- const deps = [state.sessions?.orderById, state.sessions?.pinnedIds, state.sessions?.manualOrder];
1335
+ const deps = [state.sessions?.orderById, state.sessions?.pinnedIds, (state.sessions?.sortMode && state.sessions.sortMode !== "saved" ? state.sessions.sortSnapshot : state.sessions?.manualOrder)];
1336
1336
  const cached = sessionSearchIndexCache.get(byId);
1337
1337
  if (cached && sameRowDeps(cached.deps, deps)) return cached;
1338
1338
  const expanded = buildSessionTree(
@@ -1340,7 +1340,7 @@ function sessionSearchIndex(state) {
1340
1340
  new Set(),
1341
1341
  state.sessions?.orderById,
1342
1342
  state.sessions?.pinnedIds,
1343
- state.sessions?.manualOrder,
1343
+ (state.sessions?.sortMode && state.sessions.sortMode !== "saved" ? state.sessions.sortSnapshot : state.sessions?.manualOrder),
1344
1344
  );
1345
1345
  const parentById = new Map();
1346
1346
  const entryById = new Map();
@@ -1371,7 +1371,7 @@ function buildSearchedSessionFlat(state, query) {
1371
1371
  if (!query) return existingFlat;
1372
1372
  const cached = searchedSessionFlatCache.get(existingFlat);
1373
1373
  const deps = [state.sessions?.byId, state.sessions?.ownerFilter, state.auth, state.sessions?.orderById,
1374
- state.sessions?.pinnedIds, state.sessions?.manualOrder, state.sessions?.filterExceptionId];
1374
+ state.sessions?.pinnedIds, (state.sessions?.sortMode && state.sessions.sortMode !== "saved" ? state.sessions.sortSnapshot : state.sessions?.manualOrder), state.sessions?.filterExceptionId];
1375
1375
  if (cached?.query === query && sameRowDeps(cached.deps, deps)) return cached.result;
1376
1376
 
1377
1377
  const byId = state.sessions?.byId || {};
@@ -0,0 +1,45 @@
1
+ import { buildSessionTree } from "./session-tree.js";
2
+
3
+ export const normalizeSessionSortMode = mode => ["used", "updated"].includes(mode) ? mode : "saved";
4
+ export function normalizeSessionUsage(value) {
5
+ return Object.fromEntries(Object.entries(value || {}).filter(([id, at]) => id && Number.isFinite(at) && at > 0));
6
+ }
7
+ export function snapshotSessionOrder(sessions, mode) {
8
+ const saved = buildSessionTree(Object.values(sessions.byId), new Set(), sessions.orderById, sessions.pinnedIds, sessions.manualOrder);
9
+ const rank = new Map(saved.map((row, index) => [row.sessionId, index]));
10
+ const timestamp = session => {
11
+ if (mode === "used") return sessions.usedAt?.[session.sessionId] || 0;
12
+ const raw = session.updatedAt ?? session.summaryUpdatedAt ?? session.latestSummaryUpdatedAt;
13
+ const value = typeof raw === "number" ? raw : Date.parse(raw || "");
14
+ return Number.isFinite(value) ? value : 0;
15
+ };
16
+ return Object.values(sessions.byId).sort((a, b) => timestamp(b) - timestamp(a)
17
+ || (rank.get(a.sessionId) ?? Infinity) - (rank.get(b.sessionId) ?? Infinity))
18
+ .map(session => session.sessionId);
19
+ }
20
+
21
+ // Live catalog and usage changes update row contents, never existing positions.
22
+ // A mode change or explicit refresh captures the next order. New rows append.
23
+ export function reconcileSessionSort(previous, next, action) {
24
+ const sessions = next.sessions;
25
+ if (!sessions) return next;
26
+ const mode = normalizeSessionSortMode(sessions.sortMode);
27
+ const modeChanged = mode !== normalizeSessionSortMode(previous.sessions?.sortMode);
28
+ const refresh = action.type === "sessions/refreshSort";
29
+ const rebuild = sessions.flat !== previous.sessions?.flat || sessions.byId !== previous.sessions?.byId || modeChanged || refresh;
30
+ if (!rebuild) return next;
31
+ let sortSnapshot = sessions.sortSnapshot;
32
+ if (mode !== "saved") {
33
+ if (!sessions.listingSeen) sortSnapshot = [];
34
+ else if (modeChanged || refresh || !sortSnapshot?.length) sortSnapshot = snapshotSessionOrder(sessions, mode);
35
+ else {
36
+ const known = new Set(sortSnapshot);
37
+ const added = Object.keys(sessions.byId).filter(id => !known.has(id));
38
+ if (added.length) sortSnapshot = [...sortSnapshot, ...added];
39
+ }
40
+ }
41
+ const flat = mode === "saved" && !modeChanged ? sessions.flat : buildSessionTree(
42
+ Object.values(sessions.byId), sessions.collapsedIds, sessions.orderById,
43
+ sessions.pinnedIds, mode === "saved" ? sessions.manualOrder : sortSnapshot);
44
+ return { ...next, sessions: { ...sessions, sortSnapshot, flat } };
45
+ }
@@ -480,6 +480,9 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
480
480
  pinnedIds: normalizeStoredPinnedSessionIds(pinnedSessionIds),
481
481
  // Explicit user placement (desktop drag); read by every surface.
482
482
  manualOrder: normalizeStoredSessionOrder(sessionOrder),
483
+ sortMode: "saved",
484
+ sortSnapshot: [],
485
+ usedAt: {},
483
486
  // Whether a listing of each kind has been processed. "This row is
484
487
  // new" is only answerable against a previous listing OF THE SAME
485
488
  // KIND — inferring it from byId being non-empty was wrong, because
@@ -1,3 +1,4 @@
1
+ import { normalizeSessionSortMode, normalizeSessionUsage } from "../../core/src/session-sort.js";
1
2
  import { ChatCallLine } from "./chat-call-line.js";
2
3
  import React from "react";
3
4
  import { FeatureFlagsPanel } from "./feature-flags-panel.js";
@@ -291,6 +292,8 @@ function clearBrowserPreferenceCache() {
291
292
  function normalizeProfileSettings(settings) {
292
293
  const candidate = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
293
294
  const normalized = {};
295
+ if (hasOwn(candidate, "sessionSortMode")) normalized.sessionSortMode = normalizeSessionSortMode(candidate.sessionSortMode);
296
+ if (hasOwn(candidate, "sessionUsedAt")) normalized.sessionUsedAt = normalizeSessionUsage(candidate.sessionUsedAt);
294
297
  if (hasOwn(candidate, "moa")) normalized.moa = normalizeMoa(candidate.moa);
295
298
  if (typeof candidate.themeId === "string" && candidate.themeId.trim()) {
296
299
  normalized.themeId = candidate.themeId.trim();
@@ -441,7 +444,7 @@ function profileViewState(root) {
441
444
  return {
442
445
  ...root.ui, ...root.ui.layout,
443
446
  ownerFilter: root.sessions.ownerFilter, pinnedIds: root.sessions.pinnedIds,
444
- manualOrder: root.sessions.manualOrder, collapsedSessionIds: root.sessions.collapsedIds,
447
+ manualOrder: root.sessions.manualOrder, sessionSortMode: root.sessions.sortMode, sessionUsedAt: root.sessions.usedAt, collapsedSessionIds: root.sessions.collapsedIds,
445
448
  activeSessionId: root.sessions.activeSessionId, canvasPrefs: root.canvas?.prefs,
446
449
  };
447
450
  }
@@ -464,6 +467,8 @@ function profileSettingsFromViewState(state, preservedOtherTouchScale = null, pr
464
467
  },
465
468
  pinnedSessionIds: state.pinnedIds,
466
469
  sessionOrder: state.manualOrder,
470
+ sessionSortMode: state.sessionSortMode,
471
+ sessionUsedAt: state.sessionUsedAt,
467
472
  collapsedSessionIds: state.collapsedSessionIds,
468
473
  activeSessionId: state.activeSessionId,
469
474
  // rightPaneMode describes the DESKTOP right column; a phone has no
@@ -528,6 +533,8 @@ function buildDefaultProfileSettingsFromState(state, preservedOtherTouchScale =
528
533
  layoutAdjustments: state?.ui?.layout,
529
534
  pinnedSessionIds: state?.sessions?.pinnedIds,
530
535
  sessionOrder: state?.sessions?.manualOrder,
536
+ sessionSortMode: state?.sessions?.sortMode,
537
+ sessionUsedAt: state?.sessions?.usedAt,
531
538
  collapsedSessionIds: state?.sessions?.collapsedIds,
532
539
  activeSessionId: state?.sessions?.activeSessionId,
533
540
  ...(isNarrowViewport()
@@ -575,6 +582,8 @@ function materializeProfileSettings(remoteSettings, defaults) {
575
582
  // here is DROPPED at startup — and the save effect then writes the
576
583
  // empty value straight back over the stored one. Leaving sessionOrder
577
584
  // out made every placement survive exactly until the next reload.
585
+ sessionSortMode: normalizedRemote.sessionSortMode ?? normalizedDefaults.sessionSortMode,
586
+ sessionUsedAt: normalizedRemote.sessionUsedAt ?? normalizedDefaults.sessionUsedAt,
578
587
  sessionOrder: hasOwn(normalizedRemote, "sessionOrder")
579
588
  ? normalizedRemote.sessionOrder
580
589
  : normalizedDefaults.sessionOrder,
@@ -2030,6 +2039,10 @@ function ArtifactLink({ artifactRef }) {
2030
2039
  */
2031
2040
  function openArtifactFromChat(controller, sessionId, filename) {
2032
2041
  if (!controller?.revealArtifact) return;
2042
+ if (controller.openChatArtifact) {
2043
+ Promise.resolve(controller.openChatArtifact(sessionId, filename)).catch(() => {});
2044
+ return;
2045
+ }
2033
2046
  const isPhone = typeof window !== "undefined"
2034
2047
  && typeof window.matchMedia === "function"
2035
2048
  && window.matchMedia("(max-width: 920px)").matches;
@@ -4545,28 +4558,26 @@ function SessionDetailBox({ session, childCount = 0, pause = null, controller =
4545
4558
  * fleet. That only holds while every prop is referentially stable — hence the
4546
4559
  * hoisted click handler and ref setter rather than closures built per row.
4547
4560
  */
4548
- /**
4549
- * True when the primary input is a finger. Drag-to-folder is armed on
4550
- * pointerdown and needs `touch-action: none` to receive a move stream — which
4551
- * on a touch screen also means the list can no longer be scrolled by dragging
4552
- * it, so the gesture is withheld there entirely.
4553
- */
4554
- function useCoarsePointer() {
4555
- const query = "(pointer: coarse)";
4561
+ /** Subscribe to viewport or input changes without remounting the pane. */
4562
+ function useMediaQuery(query) {
4556
4563
  const read = () => (typeof window !== "undefined" && typeof window.matchMedia === "function"
4557
4564
  ? window.matchMedia(query).matches
4558
4565
  : false);
4559
- const [coarse, setCoarse] = React.useState(read);
4566
+ const [matches, setMatches] = React.useState(read);
4560
4567
  React.useEffect(() => {
4561
4568
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
4562
4569
  const media = window.matchMedia(query);
4563
- const onChange = (event) => setCoarse(event.matches);
4570
+ const onChange = (event) => setMatches(event.matches);
4571
+ setMatches(media.matches);
4564
4572
  media.addEventListener?.("change", onChange);
4565
4573
  return () => media.removeEventListener?.("change", onChange);
4566
- }, []);
4567
- return coarse;
4574
+ }, [query]);
4575
+ return matches;
4568
4576
  }
4569
4577
 
4578
+ // Withhold drag-to-folder on touch screens so finger drags can scroll the list.
4579
+ function useCoarsePointer() { return useMediaQuery("(pointer: coarse)"); }
4580
+
4570
4581
  const RAIL_ORIGIN_PX = 13;
4571
4582
  const RAIL_STEP_PX = 11;
4572
4583
  const RAIL_WIDTH_PX = 1.5;
@@ -5039,11 +5050,10 @@ function useAxisLockedPan(ref, enabled = true) {
5039
5050
  }, [ref, enabled]);
5040
5051
  }
5041
5052
 
5042
- const SessionSearchControl = React.memo(function SessionSearchControl({ query = "", onQuery, matchCount = 0, mobile = false, listRef = null }) {
5053
+ const SessionSearchControl = React.memo(function SessionSearchControl({ query = "", onQuery, matchCount = 0, mobile = false, expanded = false, onOpen, onClose, triggerRef, listRef = null }) {
5043
5054
  // Keep typing local: expensive list filtering and external-store updates
5044
5055
  // must never be part of the input's immediate render path.
5045
5056
  const [draft, setDraft] = React.useState(query);
5046
- const [mobileOpen, setMobileOpen] = React.useState(Boolean(query));
5047
5057
  const timer = React.useRef(null);
5048
5058
  const published = React.useRef(query);
5049
5059
  const composing = React.useRef(false);
@@ -5062,29 +5072,24 @@ const SessionSearchControl = React.memo(function SessionSearchControl({ query =
5062
5072
  cancel(); published.current = query; setDraft(query);
5063
5073
  }, [query]);
5064
5074
  const inputRef = React.useRef(null);
5065
- React.useEffect(() => {
5066
- if (query) setMobileOpen(true);
5067
- }, [query]);
5068
- const open = () => {
5069
- setMobileOpen(true);
5070
- requestAnimationFrame(() => inputRef.current?.focus({ preventScroll: true }));
5071
- };
5072
5075
  const closeOrClear = () => {
5073
5076
  if (draft || query) { setDraft(""); publish(""); }
5074
- else setMobileOpen(false);
5077
+ else onClose?.();
5075
5078
  };
5076
5079
  const resultLabel = draft !== query ? "Searching…" : draft
5077
5080
  ? `${matchCount} ${matchCount === 1 ? "match" : "matches"}` : "";
5078
5081
  return React.createElement("div", {
5079
- className: `ps-session-search${mobileOpen ? " is-open" : ""}`,
5082
+ className: `ps-session-search${expanded ? " is-open" : ""}`,
5080
5083
  "data-session-search": "true",
5081
5084
  },
5082
5085
  mobile ? React.createElement("button", {
5083
5086
  type: "button",
5084
5087
  className: "ps-session-search-trigger",
5088
+ ref: triggerRef,
5089
+ "aria-expanded": expanded,
5085
5090
  "aria-label": "Search sessions",
5086
5091
  title: "Search sessions",
5087
- onClick: open,
5092
+ onClick: onOpen,
5088
5093
  }, React.createElement(SearchGlyph)) : null,
5089
5094
  React.createElement("div", { className: "ps-session-search-fields" },
5090
5095
  React.createElement(SearchGlyph),
@@ -5115,7 +5120,7 @@ const SessionSearchControl = React.memo(function SessionSearchControl({ query =
5115
5120
  role: "status",
5116
5121
  "aria-live": "polite",
5117
5122
  }, resultLabel) : null,
5118
- (draft || mobile) ? React.createElement("button", {
5123
+ draft ? React.createElement("button", {
5119
5124
  type: "button",
5120
5125
  className: "ps-session-search-clear",
5121
5126
  "aria-label": draft ? "Clear session search" : "Close session search",
@@ -5132,6 +5137,11 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5132
5137
  // sessions-ONLY layout is the exception: it has the whole screen and the
5133
5138
  // detail box is the point of asking for it.
5134
5139
  const isMobilePane = String(panelClassName).includes("ps-mobile-session-pane");
5140
+ const compactSearch = useMediaQuery("(max-width: 920px)");
5141
+ const [searchOpen, setSearchOpen] = React.useState(false);
5142
+ const searchOverlay = compactSearch && searchOpen;
5143
+ const searchDialogRef = React.useRef(null);
5144
+ const searchTriggerRef = React.useRef(null);
5135
5145
  // Vertical is the primary scroll axis; sideways takes a deliberate swipe.
5136
5146
  const sessionListRef = React.useRef(null);
5137
5147
  // Selection reverts to plain taps wherever the primary input is a finger:
@@ -5159,6 +5169,8 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5159
5169
  ownerFilter: state.sessions.ownerFilter,
5160
5170
  pinnedIds: state.sessions.pinnedIds,
5161
5171
  manualOrder: state.sessions.manualOrder,
5172
+ sortMode: state.sessions.sortMode,
5173
+ sortSnapshot: state.sessions.sortSnapshot,
5162
5174
  selectedIds: state.sessions.selectedIds,
5163
5175
  selectMode: state.sessions.selectMode,
5164
5176
  auth: state.auth,
@@ -5198,6 +5210,8 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5198
5210
  ownerFilter: viewState.ownerFilter,
5199
5211
  pinnedIds: viewState.pinnedIds,
5200
5212
  manualOrder: viewState.manualOrder,
5213
+ sortMode: viewState.sortMode,
5214
+ sortSnapshot: viewState.sortSnapshot,
5201
5215
  selectedIds: viewState.selectedIds,
5202
5216
  selectMode: viewState.selectMode,
5203
5217
  listDeselected: viewState.listDeselected,
@@ -5216,7 +5230,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5216
5230
  prefs: viewState.canvasPrefs,
5217
5231
  },
5218
5232
  budget: { paused: viewState.budgetPaused },
5219
- }), [viewState.activeSessionId, viewState.auth, viewState.branding, viewState.budgetPaused, viewState.canvasBySessionId, viewState.canvasPrefs, viewState.connectionMode, viewState.filterQuery, viewState.listDeselected, viewState.ownerFilter, viewState.pinnedIds, viewState.manualOrder, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
5233
+ }), [viewState.activeSessionId, viewState.auth, viewState.branding, viewState.budgetPaused, viewState.canvasBySessionId, viewState.canvasPrefs, viewState.connectionMode, viewState.filterQuery, viewState.listDeselected, viewState.ownerFilter, viewState.pinnedIds, viewState.manualOrder, viewState.sortMode, viewState.sortSnapshot, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
5220
5234
  // Hold the previous rows when a poll produced identical output.
5221
5235
  const rows = useStableValue(computedRows);
5222
5236
  const searchScrollRef = React.useRef({ top: 0, left: 0, restore: false });
@@ -5236,6 +5250,35 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5236
5250
  }
5237
5251
  searchEnvRef.current?.setQuery?.(next);
5238
5252
  }, []);
5253
+ const closeSearch = React.useCallback(() => {
5254
+ setSearchOpen(false);
5255
+ setSearchQuery("");
5256
+ requestAnimationFrame(() => searchTriggerRef.current?.focus({ preventScroll: true }));
5257
+ }, [setSearchQuery]);
5258
+ React.useLayoutEffect(() => {
5259
+ const dialog = searchDialogRef.current;
5260
+ if (!searchOverlay || !dialog) return undefined;
5261
+ const viewport = window.visualViewport;
5262
+ const resize = () => {
5263
+ dialog.style.top = `${(viewport?.offsetTop || 0) + 8}px`;
5264
+ dialog.style.left = `${(viewport?.offsetLeft || 0) + 8}px`;
5265
+ dialog.style.width = `${(viewport?.width || window.innerWidth) - 16}px`;
5266
+ dialog.style.height = `${(viewport?.height || window.innerHeight) - 16}px`;
5267
+ };
5268
+ resize();
5269
+ dialog.showModal();
5270
+ dialog.querySelector("input")?.focus({ preventScroll: true });
5271
+ viewport?.addEventListener("resize", resize);
5272
+ viewport?.addEventListener("scroll", resize);
5273
+ window.addEventListener("resize", resize);
5274
+ return () => {
5275
+ viewport?.removeEventListener("resize", resize);
5276
+ viewport?.removeEventListener("scroll", resize);
5277
+ window.removeEventListener("resize", resize);
5278
+ dialog.close();
5279
+ };
5280
+ }, [searchOverlay]);
5281
+ React.useEffect(() => { if (!compactSearch) setSearchOpen(false); }, [compactSearch]);
5239
5282
  React.useLayoutEffect(() => {
5240
5283
  if (viewState.filterQuery || !searchScrollRef.current.restore || !sessionListRef.current) return;
5241
5284
  sessionListRef.current.scrollTop = searchScrollRef.current.top;
@@ -5369,7 +5412,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5369
5412
  // rows/viewState from a ref that is refreshed on every render instead of
5370
5413
  // closing over them.
5371
5414
  const clickEnv = React.useRef(null);
5372
- clickEnv.current = { rows, viewState, controller, selection };
5415
+ clickEnv.current = { rows, viewState, controller, selection, searchOverlay, closeSearch };
5373
5416
  // ── Drag sessions into / out of groups ────────────────────────────
5374
5417
  // Pointer-driven so it works from <button> rows, can render a collection
5375
5418
  // ghost for a multi-selection, and can highlight the destination folder.
@@ -5695,6 +5738,15 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5695
5738
 
5696
5739
  const handleRowClick = React.useCallback((event, row) => {
5697
5740
  const { rows: currentRows, viewState: current, controller: ctl, selection: pick } = clickEnv.current;
5741
+ if (clickEnv.current.searchOverlay && !row.isGroup) {
5742
+ if (pick) pick.onSelect(row.sessionId);
5743
+ else {
5744
+ ctl.setFocus("sessions");
5745
+ ctl.loadSession(row.sessionId).catch(() => {});
5746
+ }
5747
+ clickEnv.current.closeSearch();
5748
+ return;
5749
+ }
5698
5750
  if (pick) {
5699
5751
  if (row.isGroup || (row.hasChildren && row.active)) ctl.dispatch({ type: row.collapsed ? "sessions/expand" : "sessions/collapse", sessionId: row.sessionId });
5700
5752
  if (!row.isGroup) pick.onSelect(row.sessionId);
@@ -5938,14 +5990,12 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5938
5990
  : "Release to cancel"))
5939
5991
  : null;
5940
5992
 
5941
- return React.createElement(React.Fragment, null,
5942
- dragGhost,
5943
- actionsOnly ? (actionsHost ? createPortal(React.createElement("div", { className: "ps-moa-control-actions", onClick: onAction }, panelActions), actionsHost) : null) : React.createElement(Panel, {
5944
- title: [{ text: "Sessions", color: "yellow", bold: true }],
5993
+ const sessionPanel = actionsOnly ? (actionsHost ? createPortal(React.createElement("div", { className: "ps-moa-control-actions", onClick: onAction }, panelActions), actionsHost) : null) : React.createElement(Panel, {
5994
+ title: [{ text: searchOverlay ? "Find a session" : "Sessions", color: "yellow", bold: true }],
5945
5995
  color: "yellow",
5946
5996
  focused: viewState.focused,
5947
5997
  theme,
5948
- actions: selection ? React.createElement(React.Fragment, null,
5998
+ actions: searchOverlay ? React.createElement(IconButton, { label: "Close session search", icon: "×", onClick: closeSearch }) : selection ? React.createElement(React.Fragment, null,
5949
5999
  selection.onCreate ? React.createElement(IconButton, {
5950
6000
  className: "ps-mini-button", icon: React.createElement(PlusGlyph),
5951
6001
  label: "Create New Session", onClick: selection.onCreate,
@@ -5991,16 +6041,45 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5991
6041
  setRef: setSessionButtonRef,
5992
6042
  // Drag-to-folder is a fine-pointer gesture: arming it on
5993
6043
  // touch hijacks the finger that should be scrolling the list.
5994
- drag: selection || touchInput ? null : dragHandlers,
6044
+ drag: selection || touchInput || viewState.sortMode !== "saved" ? null : dragHandlers,
5995
6045
  }))),
5996
- React.createElement(SessionSearchControl, {
5997
- query: viewState.filterQuery,
5998
- onQuery: setSearchQuery,
5999
- matchCount: searchMatchCount,
6000
- mobile: isMobilePane,
6001
- listRef: sessionListRef,
6002
- }),
6003
- (showDetailBox === null ? !isMobilePane : showDetailBox)
6046
+ React.createElement("div", {
6047
+ className: "ps-session-find-controls",
6048
+ onPointerDown: (event) => {
6049
+ // Keep the search keyboard open while tapping sort, refresh, or clear.
6050
+ // Safari otherwise blurs the field without focusing the button.
6051
+ if (compactSearch && event.button === 0 && event.target.closest?.("button")
6052
+ && event.currentTarget.contains(document.activeElement)
6053
+ && document.activeElement?.matches("input")) event.preventDefault();
6054
+ },
6055
+ },
6056
+ React.createElement("div", { className: "ps-session-sort-controls" },
6057
+ React.createElement("div", { className: "ps-session-sort-modes", role: "group", "aria-label": "Session sort order" },
6058
+ ...[["used", "Recently used", "M12 8v4l3 2", true], ["updated", "Recently updated", "M3 12h4l3-7 4 14 3-7h4"], ["saved", "Saved order", "M6 3h12v18l-6-4-6 4Z"]].map(([mode, label, path, clock]) => React.createElement(IconButton, {
6059
+ key: mode, label, className: "ps-session-sort-button", active: (viewState.sortMode || "saved") === mode, pressed: (viewState.sortMode || "saved") === mode,
6060
+ icon: React.createElement("svg", { viewBox: "0 0 24 24", width: 18, height: 18, fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, clock ? React.createElement("circle", { cx: 12, cy: 12, r: 9 }) : null, React.createElement("path", { d: path })),
6061
+ onClick: () => controller.dispatch({ type: "sessions/sortMode", mode }),
6062
+ }))),
6063
+ React.createElement(IconButton, {
6064
+ label: "Refresh session order", className: "ps-session-sort-refresh",
6065
+ icon: React.createElement("svg", { viewBox: "0 0 24 24", width: 18, height: 18, fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, React.createElement("path", { d: "M20 7v5h-5M20 12a8 8 0 1 0-2 5M20 7v5" })),
6066
+ onClick: async () => {
6067
+ try { await controller.refreshSessions(); controller.dispatch({ type: "sessions/refreshSort" }); }
6068
+ catch { controller.dispatch({ type: "ui/status", text: "Could not refresh sessions. Try again." }); }
6069
+ },
6070
+ })),
6071
+ React.createElement(SessionSearchControl, {
6072
+ query: viewState.filterQuery,
6073
+ onQuery: setSearchQuery,
6074
+ matchCount: searchMatchCount,
6075
+ mobile: compactSearch,
6076
+ expanded: searchOverlay,
6077
+ onOpen: () => setSearchOpen(true),
6078
+ onClose: closeSearch,
6079
+ triggerRef: searchTriggerRef,
6080
+ listRef: sessionListRef,
6081
+ })),
6082
+ !searchOverlay && (showDetailBox === null ? !isMobilePane : showDetailBox)
6004
6083
  ? React.createElement(SessionDetailBox, {
6005
6084
  session: activeSession,
6006
6085
  childCount: activeRow?.childCount || 0,
@@ -6014,7 +6093,21 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
6014
6093
  collapsed: !detailCollapsed,
6015
6094
  }),
6016
6095
  })
6017
- : null),
6096
+ : null);
6097
+
6098
+ return React.createElement(React.Fragment, null,
6099
+ dragGhost,
6100
+ !searchOverlay ? sessionPanel : null,
6101
+ React.createElement("dialog", {
6102
+ ref: searchDialogRef,
6103
+ className: "ps-session-search-overlay",
6104
+ "aria-label": "Find a session",
6105
+ onCancel: event => { event.preventDefault(); closeSearch(); },
6106
+ onKeyDownCapture: event => {
6107
+ if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeSearch(); }
6108
+ },
6109
+ onClick: event => { if (event.target === event.currentTarget) closeSearch(); },
6110
+ }, searchOverlay ? sessionPanel : null),
6018
6111
  (manageOpen && activeSession && !activeSession.isGroup)
6019
6112
  ? React.createElement(SessionModifyModal, {
6020
6113
  controller,
@@ -7192,14 +7285,14 @@ export function evaluateKeyboardTakeover(baseline, viewportWidth, viewportHeight
7192
7285
  }
7193
7286
 
7194
7287
  /**
7195
- * True while the on-screen keyboard is up AND the chat composer summoned it.
7288
+ * True while the on-screen keyboard is up AND a matching control has focus.
7196
7289
  * Both conditions matter: height alone would fire for a modal's text field
7197
7290
  * and collapse the workspace behind the modal; focus alone would fire for
7198
7291
  * hardware keyboards that shrink nothing. State-driven on the keyboard
7199
7292
  * itself, so it reverts the moment the keyboard goes away — including the
7200
7293
  * iOS swipe-dismiss that closes the keyboard without blurring the input.
7201
7294
  */
7202
- function useKeyboardTakeover(enabled) {
7295
+ function useKeyboardTakeover(enabled, focusSelector = ".ps-prompt-input") {
7203
7296
  const [takeover, setTakeover] = React.useState(false);
7204
7297
  React.useEffect(() => {
7205
7298
  if (!enabled || typeof window === "undefined" || !window.visualViewport) {
@@ -7211,7 +7304,7 @@ function useKeyboardTakeover(enabled) {
7211
7304
  let frame = 0;
7212
7305
  const evaluate = () => {
7213
7306
  frame = 0;
7214
- const composerFocused = Boolean(document.activeElement?.classList?.contains("ps-prompt-input"));
7307
+ const composerFocused = Boolean(document.activeElement?.matches?.(focusSelector));
7215
7308
  const next = evaluateKeyboardTakeover(baseline, viewport.width, viewport.height, composerFocused);
7216
7309
  baseline = next.baseline;
7217
7310
  setTakeover(next.takeover);
@@ -7229,7 +7322,7 @@ function useKeyboardTakeover(enabled) {
7229
7322
  window.removeEventListener("focusin", schedule);
7230
7323
  window.removeEventListener("focusout", schedule);
7231
7324
  };
7232
- }, [enabled]);
7325
+ }, [enabled, focusSelector]);
7233
7326
  return enabled ? takeover : false;
7234
7327
  }
7235
7328
 
@@ -9381,7 +9474,7 @@ function StatusStrip({ controller }) {
9381
9474
  // (hold ~450ms to see the label, release to dismiss — the long-press does not
9382
9475
  // fire onClick). aria-label carries the meaning for assistive tech.
9383
9476
  const ICON_HOVER_TOOLTIP_MS = 1000;
9384
- function IconButton({ icon, label, onClick, disabled = false, active = false, className = "ps-toolbar-button" }) {
9477
+ function IconButton({ icon, label, onClick, disabled = false, active = false, pressed = undefined, className = "ps-toolbar-button" }) {
9385
9478
  // The tooltip is portaled to <body> so it escapes the toolbar/pane
9386
9479
  // overflow-clipping and stacking contexts (nested tooltips were hidden
9387
9480
  // behind, or bled through by, the panes). Coordinates are computed from
@@ -9524,6 +9617,7 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
9524
9617
  onClick: handleClick,
9525
9618
  disabled,
9526
9619
  "aria-label": label,
9620
+ "aria-pressed": pressed,
9527
9621
  onPointerEnter: startHover,
9528
9622
  onPointerLeave: endHover,
9529
9623
  onPointerDown: startPress,
@@ -14666,6 +14760,8 @@ export function PilotSwarmWebApp({ controller, suspended = false, moa = null })
14666
14760
  // never persists — the reducer would reorder the list correctly and
14667
14761
  // the placement would vanish on reload.
14668
14762
  manualOrder: rootState.sessions.manualOrder,
14763
+ sessionSortMode: rootState.sessions.sortMode,
14764
+ sessionUsedAt: rootState.sessions.usedAt,
14669
14765
  // Pass the live Set reference here so shallow-equal sees the same
14670
14766
  // identity across renders (a fresh array would break memoization).
14671
14767
  // Conversion to a sorted array happens inside `normalizeProfileSettings`.
@@ -15007,7 +15103,7 @@ export function PilotSwarmWebApp({ controller, suspended = false, moa = null })
15007
15103
  });
15008
15104
  }, 400);
15009
15105
  return undefined;
15010
- }, [controller, state.moa, state.activeSessionId, state.activityPaneAdjust, state.agentPickerUsage, state.canvasOpen, state.canvasPaneAdjust, state.canvasPrefs, state.canvasZen, state.collapsedSessionIds, state.diagnosticsOpen, state.diagnosticsPaneAdjust, state.diagnosticsSplitAdjust, state.ownerFilter, state.paneAdjust, state.manualOrder, state.pinnedIds, state.portalSessionColumnAdjust, state.rightPaneMode, state.sessionDetailCollapsed, state.sessionPaneAdjust, state.sessionViews, state.themeId, state.touchScale]);
15106
+ }, [controller, state.moa, state.activeSessionId, state.activityPaneAdjust, state.agentPickerUsage, state.canvasOpen, state.canvasPaneAdjust, state.canvasPrefs, state.canvasZen, state.collapsedSessionIds, state.diagnosticsOpen, state.diagnosticsPaneAdjust, state.diagnosticsSplitAdjust, state.ownerFilter, state.paneAdjust, state.manualOrder, state.sessionSortMode, state.sessionUsedAt, state.pinnedIds, state.portalSessionColumnAdjust, state.rightPaneMode, state.sessionDetailCollapsed, state.sessionPaneAdjust, state.sessionViews, state.themeId, state.touchScale]);
15011
15107
 
15012
15108
  React.useEffect(() => {
15013
15109
  applyDocumentTheme(state.themeId);
@@ -1 +1 @@
1
- import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-CDyGPb8L.js";import{p as f}from"./chunk-JWPE2WC7-AhysI3TD.js";import{_ as n,l as o}from"./mermaid.core-BPC8y2lr.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-CHVeQsGv.js";import"./pilotswarm-BnfVgmru.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
1
+ import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-BOfHzBMe.js";import{p as f}from"./chunk-JWPE2WC7-ZfmDblU2.js";import{_ as n,l as o}from"./mermaid.core-D7ywN4EU.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-CD5a_UMt.js";import"./pilotswarm-Dccv5YRO.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};