pilotswarm 0.5.7 → 0.5.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
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",
@@ -77,7 +77,7 @@
77
77
  "hono": "^4.12.10",
78
78
  "ink": "^6.8.0",
79
79
  "jose": "^6.2.2",
80
- "pilotswarm-sdk": "^0.5.7",
80
+ "pilotswarm-sdk": "^0.5.9",
81
81
  "react": "^19.2.4",
82
82
  "react-dom": "^19.2.4",
83
83
  "ws": "^8.18.2"
@@ -467,6 +467,12 @@ function ownerDisplayName(owner, fallback = "unknown user") {
467
467
  return String(owner?.displayName || owner?.email || "").trim() || fallback;
468
468
  }
469
469
 
470
+ // Owner key of the first-class System user. System-agent children inherit this
471
+ // owner; they belong to the static "System" filter entry, so we never mint a
472
+ // second, duplicate "System" owner bucket for them. Computed via the join
473
+ // helper so it stays byte-identical to real owner keys.
474
+ const SYSTEM_OWNER_KEY = ownerKeyForPrincipal({ provider: "system", subject: "system" });
475
+
470
476
  function buildSessionOwnerFilterItems(state) {
471
477
  const principal = state.auth?.principal || null;
472
478
  const principalKey = ownerKeyForPrincipal(principal);
@@ -506,7 +512,9 @@ function buildSessionOwnerFilterItems(state) {
506
512
  for (const session of Object.values(state.sessions?.byId || {})) {
507
513
  const owner = session?.owner;
508
514
  const key = ownerKeyForPrincipal(owner);
509
- if (!key || key === principalKey || ownersByKey.has(key)) continue;
515
+ // Skip the current user (covered by "Me") and the System user (covered
516
+ // by the static "System" entry — never a duplicate owner bucket).
517
+ if (!key || key === principalKey || key === SYSTEM_OWNER_KEY || ownersByKey.has(key)) continue;
510
518
  ownersByKey.set(key, owner);
511
519
  }
512
520
 
@@ -527,7 +535,7 @@ function buildSessionOwnerFilterItems(state) {
527
535
  return items;
528
536
  }
529
537
 
530
- function defaultOwnerFilterForPrincipal(principal) {
538
+ export function defaultOwnerFilterForPrincipal(principal) {
531
539
  return ownerKeyForPrincipal(principal)
532
540
  ? {
533
541
  all: false,
@@ -1574,6 +1582,36 @@ export class PilotSwarmUiController {
1574
1582
  });
1575
1583
  }
1576
1584
 
1585
+ // Keep the open Session Filter modal's entry list in sync with the live
1586
+ // session set. The item list is snapshotted into modal.items at open time
1587
+ // (it must be — the shared keyboard-nav handler indexes into it), but the
1588
+ // periodic catalog refresh mutates owners underneath it: a user whose first
1589
+ // session arrives after the modal opened would otherwise never see their
1590
+ // owner bucket without reopening. Rebuild in place, preserving the
1591
+ // highlighted row by its stable id, and only when the shape actually
1592
+ // changed (so we don't churn the modal on every 4s refresh).
1593
+ refreshOpenSessionOwnerFilterModal() {
1594
+ const state = this.getState();
1595
+ const modal = state.ui.modal;
1596
+ if (!modal || modal.type !== "sessionOwnerFilter") return;
1597
+ const nextItems = buildSessionOwnerFilterItems(state);
1598
+ const prevItems = Array.isArray(modal.items) ? modal.items : [];
1599
+ const sameShape = prevItems.length === nextItems.length
1600
+ && prevItems.every((item, i) => item?.id === nextItems[i]?.id);
1601
+ if (sameShape) return;
1602
+ const prevSelectedId = prevItems[Math.max(0, Number(modal.selectedIndex) || 0)]?.id;
1603
+ const remappedIndex = prevSelectedId != null
1604
+ ? nextItems.findIndex((item) => item.id === prevSelectedId)
1605
+ : -1;
1606
+ const nextSelectedIndex = remappedIndex >= 0
1607
+ ? remappedIndex
1608
+ : Math.max(0, Math.min(Number(modal.selectedIndex) || 0, Math.max(0, nextItems.length - 1)));
1609
+ this.dispatch({
1610
+ type: "ui/modal",
1611
+ modal: { ...modal, items: nextItems, selectedIndex: nextSelectedIndex },
1612
+ });
1613
+ }
1614
+
1577
1615
  setFilesFilter(patch = {}) {
1578
1616
  this.dispatch({
1579
1617
  type: "files/filter",
@@ -1743,6 +1781,7 @@ export class PilotSwarmUiController {
1743
1781
  });
1744
1782
  }
1745
1783
  this.dispatch({ type: "sessions/loaded", sessions });
1784
+ this.refreshOpenSessionOwnerFilterModal();
1746
1785
  const selected = this.getState().sessions.activeSessionId;
1747
1786
  const syncedIds = new Set();
1748
1787
  if (selected) {
@@ -198,6 +198,13 @@ function ownerDisplayName(owner, fallback = "unknown user") {
198
198
  return String(owner?.displayName || owner?.email || "").trim() || fallback;
199
199
  }
200
200
 
201
+ // The owner key of the first-class System user. Sessions a system agent spawns
202
+ // on a user's behalf inherit this owner (they are NOT is_system, so they stay
203
+ // deletable) — but they belong to the same "System" bucket as the real system
204
+ // agents, so the single System filter entry covers both. Computed via the same
205
+ // join helper so it stays byte-identical to real owner keys.
206
+ const SYSTEM_OWNER_KEY = ownerKeyForOwner({ provider: "system", subject: "system" });
207
+
201
208
  function initialsFromText(value) {
202
209
  const text = String(value || "").trim();
203
210
  if (!text) return "";
@@ -335,6 +342,10 @@ function matchesOwnerFilterDirect(session, ownerFilter = {}, auth = {}, ownerOve
335
342
  if (session?.isSystem) return ownerFilter.includeSystem === true;
336
343
  const ownerKey = ownerKeyForOwner(ownerOverride !== undefined ? ownerOverride : session?.owner);
337
344
  if (!ownerKey) return ownerFilter.includeUnowned === true;
345
+ // Sessions OWNED BY the System user (children a system agent spawned) share
346
+ // the "System" bucket with the real is_system agents — one entry, one
347
+ // meaning. Gate them on includeSystem, never on Me / owner keys.
348
+ if (ownerKey === SYSTEM_OWNER_KEY) return ownerFilter.includeSystem === true;
338
349
  const currentUserKey = ownerKeyForOwner(auth?.principal);
339
350
  if (ownerFilter.includeMe && currentUserKey && ownerKey === currentUserKey) return true;
340
351
  return Array.isArray(ownerFilter.ownerKeys) && ownerFilter.ownerKeys.includes(ownerKey);
@@ -40,6 +40,7 @@ import {
40
40
  selectStatusBar,
41
41
  selectThemePickerModal,
42
42
  selectConfirmModal,
43
+ defaultOwnerFilterForPrincipal,
43
44
  normalizeStoredLayoutAdjustments,
44
45
  normalizeStoredPinnedSessionIds,
45
46
  } from "pilotswarm/ui-core";
@@ -204,7 +205,13 @@ function profileSettingsFromViewState(state) {
204
205
  function buildDefaultProfileSettingsFromState(state) {
205
206
  return normalizeProfileSettings({
206
207
  themeId: state?.ui?.themeId,
207
- sessionOwnerFilter: state?.sessions?.ownerFilter,
208
+ // Derive the owner-filter default from the RESOLVED principal, never a
209
+ // snapshot of state.sessions.ownerFilter — at mount that snapshot is
210
+ // still {all:true} (principal not yet applied), and using it as the
211
+ // "no persisted filter" fallback would drift an authenticated user's
212
+ // saved filter to All (or, mid-resolve, hide their own sessions). This
213
+ // guarantees the fallback is always Me+System for a signed-in user.
214
+ sessionOwnerFilter: defaultOwnerFilterForPrincipal(state?.auth?.principal ?? null),
208
215
  layoutAdjustments: state?.ui?.layout,
209
216
  pinnedSessionIds: state?.sessions?.pinnedIds,
210
217
  collapsedSessionIds: state?.sessions?.collapsedIds,
@@ -4333,6 +4340,11 @@ export function PilotSwarmWebApp({ controller }) {
4333
4340
  try {
4334
4341
  const profile = await transport.getCurrentUserProfile();
4335
4342
  if (!active) return;
4343
+ // Recompute the fallback defaults from CURRENT state each poll:
4344
+ // the principal is resolved by now (it was likely null at mount),
4345
+ // so the "no persisted filter" fallback becomes the correct
4346
+ // principal-scoped default (Me+System) instead of {all:true}.
4347
+ defaultProfileSettingsRef.current = buildDefaultProfileSettingsFromState(controller.getState());
4336
4348
  const settings = materializeProfileSettings(
4337
4349
  profile?.profileSettings,
4338
4350
  defaultProfileSettingsRef.current,