pilotswarm 0.5.14 → 0.5.16

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 (34) hide show
  1. package/mcp/README.md +2 -2
  2. package/mcp/dist/src/server.d.ts.map +1 -1
  3. package/mcp/dist/src/server.js +5 -1
  4. package/mcp/dist/src/server.js.map +1 -1
  5. package/mcp/dist/src/tools/capabilities.d.ts +1 -0
  6. package/mcp/dist/src/tools/capabilities.d.ts.map +1 -1
  7. package/mcp/dist/src/tools/capabilities.js +5 -0
  8. package/mcp/dist/src/tools/capabilities.js.map +1 -1
  9. package/mcp/dist/src/tools/groups.d.ts +5 -4
  10. package/mcp/dist/src/tools/groups.d.ts.map +1 -1
  11. package/mcp/dist/src/tools/groups.js +43 -20
  12. package/mcp/dist/src/tools/groups.js.map +1 -1
  13. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  14. package/mcp/dist/src/tools/sessions.js +4 -2
  15. package/mcp/dist/src/tools/sessions.js.map +1 -1
  16. package/package.json +2 -2
  17. package/tui/plugins/.mcp.json +7 -1
  18. package/tui/plugins/agents/deepwiki.agent.md +44 -0
  19. package/tui/src/node-sdk-transport.js +61 -8
  20. package/ui/core/src/controller.js +264 -40
  21. package/ui/core/src/formatting.js +25 -0
  22. package/ui/core/src/reducer.js +117 -8
  23. package/ui/core/src/selectors.js +76 -3
  24. package/ui/core/src/state.js +3 -0
  25. package/ui/react/src/web-app.js +363 -127
  26. package/web/api/router.js +3 -1
  27. package/web/dist/assets/index-Df1mD9p0.js +24 -0
  28. package/web/dist/assets/index-whIft7M6.css +1 -0
  29. package/web/dist/assets/pilotswarm-DR5j-z8y.js +90 -0
  30. package/web/dist/index.html +3 -3
  31. package/web/runtime.js +114 -42
  32. package/web/dist/assets/index-BnxC8cNG.js +0 -24
  33. package/web/dist/assets/index-Bx6KHIaj.css +0 -1
  34. package/web/dist/assets/pilotswarm-NE7H63ha.js +0 -90
@@ -324,6 +324,12 @@ function resolveVisibleActiveSessionId(state, fallbackSessions = []) {
324
324
  if (currentSessionId && visibleRows.some((row) => row.sessionId === currentSessionId)) {
325
325
  return currentSessionId;
326
326
  }
327
+ // A navigation intent (deep link) owns selection until it is cleared by
328
+ // manual navigation or a filter change. While one exists — pending target
329
+ // still loading, or failed — never fall back to a default selection.
330
+ if (state.sessions?.navigationIntent) {
331
+ return currentSessionId;
332
+ }
327
333
  if (currentSessionId && state.sessions?.byId?.[currentSessionId] && !hasSessionVisibilityFilter(state.sessions)) {
328
334
  return currentSessionId;
329
335
  }
@@ -361,20 +367,67 @@ function updateUiForSessionSelection(state, nextActiveSessionId) {
361
367
  }
362
368
 
363
369
  function applyVisibleSessionSelection(state, nextSessions) {
370
+ let sessions = nextSessions;
371
+
372
+ // Navigation intent (deep link) outranks in-memory selection and profile
373
+ // activeSessionId: once its target session is present, latch the selection
374
+ // onto it and mark the intent resolved.
375
+ const intent = sessions.navigationIntent || null;
376
+ const intentTargetId = intent && intent.status !== "failed" && sessions.byId?.[intent.sessionId]
377
+ ? intent.sessionId
378
+ : null;
379
+ if (intentTargetId) {
380
+ sessions = {
381
+ ...sessions,
382
+ activeSessionId: intentTargetId,
383
+ navigationIntent: intent.status === "resolved"
384
+ ? intent
385
+ : { sessionId: intent.sessionId, status: "resolved" },
386
+ };
387
+ }
388
+
389
+ // Expand ancestors BEFORE resolving visibility: a selected session inside
390
+ // a collapsed group/parent is absent from the flat tree, so resolving
391
+ // first would drop the selection instead of revealing it.
392
+ const selectionCandidateId = sessions.activeSessionId || null;
393
+ if (selectionCandidateId && sessions.byId?.[selectionCandidateId]) {
394
+ const expandedCollapsedIds = reconcileCollapsedIdsForActiveSession(sessions.collapsedIds, sessions.byId, selectionCandidateId);
395
+ if (!setsEqual(expandedCollapsedIds, sessions.collapsedIds)) {
396
+ sessions = {
397
+ ...sessions,
398
+ collapsedIds: expandedCollapsedIds,
399
+ flat: buildSessionTree(Object.values(sessions.byId || {}), expandedCollapsedIds, sessions.orderById, sessions.pinnedIds),
400
+ };
401
+ }
402
+ }
403
+
404
+ // A resolved intent target excluded by the current filters gets a
405
+ // transient exception so the linked session is shown anyway. Never
406
+ // persisted; cleared on manual navigation or filter change.
407
+ if (intentTargetId && sessions.filterExceptionId !== intentTargetId) {
408
+ const probeRows = selectSessionRows({ ...state, sessions });
409
+ if (!probeRows.some((row) => row.sessionId === intentTargetId)) {
410
+ sessions = {
411
+ ...sessions,
412
+ filterExceptionId: intentTargetId,
413
+ };
414
+ }
415
+ }
416
+
364
417
  const nextState = {
365
418
  ...state,
366
- sessions: nextSessions,
419
+ sessions,
367
420
  };
368
- const nextActiveSessionId = resolveVisibleActiveSessionId(nextState, Object.values(nextSessions.byId || {}));
369
- const nextCollapsedIds = reconcileCollapsedIdsForActiveSession(nextSessions.collapsedIds, nextSessions.byId, nextActiveSessionId);
370
- const collapsedChanged = !setsEqual(nextCollapsedIds, nextSessions.collapsedIds);
421
+ const nextActiveSessionId = resolveVisibleActiveSessionId(nextState, Object.values(sessions.byId || {}));
422
+ const nextCollapsedIds = reconcileCollapsedIdsForActiveSession(sessions.collapsedIds, sessions.byId, nextActiveSessionId);
423
+ const collapsedChanged = !setsEqual(nextCollapsedIds, sessions.collapsedIds);
371
424
  const reconciledSessions = collapsedChanged
372
425
  ? {
373
- ...nextSessions,
426
+ ...sessions,
374
427
  collapsedIds: nextCollapsedIds,
375
- flat: buildSessionTree(Object.values(nextSessions.byId || {}), nextCollapsedIds, nextSessions.orderById, nextSessions.pinnedIds),
428
+ flat: buildSessionTree(Object.values(sessions.byId || {}), nextCollapsedIds, sessions.orderById, sessions.pinnedIds),
376
429
  }
377
- : nextSessions;
430
+ : sessions;
378
431
  return {
379
432
  sessions: {
380
433
  ...reconciledSessions,
@@ -515,7 +568,15 @@ export function appReducer(state, action) {
515
568
  && (settings.chatViewMode === "summary" || settings.chatViewMode === "transcript");
516
569
  const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
517
570
  const hasCollapsed = Object.prototype.hasOwnProperty.call(settings, "collapsedSessionIds");
518
- const hasActive = Object.prototype.hasOwnProperty.call(settings, "activeSessionId");
571
+ // A pending/resolved deep-link intent outranks the profile's
572
+ // persisted activeSessionId — a remote profile poll must not
573
+ // yank selection away from the linked session.
574
+ const navigationIntentLatched = Boolean(
575
+ state.sessions.navigationIntent
576
+ && state.sessions.navigationIntent.status !== "failed",
577
+ );
578
+ const hasActive = Object.prototype.hasOwnProperty.call(settings, "activeSessionId")
579
+ && !navigationIntentLatched;
519
580
  const hasLoadedSessions = Object.keys(state.sessions.byId || {}).length > 0;
520
581
  const nextLayout = hasLayout
521
582
  ? {
@@ -693,6 +754,10 @@ export function appReducer(state, action) {
693
754
  const nextSessions = {
694
755
  ...state.sessions,
695
756
  filterQuery: typeof action.query === "string" ? action.query : "",
757
+ // A filter change is an explicit user action: it releases
758
+ // the deep-link latch and its transient filter exception.
759
+ navigationIntent: null,
760
+ filterExceptionId: null,
696
761
  };
697
762
  const selection = applyVisibleSessionSelection(state, nextSessions);
698
763
  return {
@@ -708,6 +773,8 @@ export function appReducer(state, action) {
708
773
  ...state.sessions,
709
774
  ownerFilterExplicit: true,
710
775
  ownerFilter: normalizeSessionOwnerFilter(action.filter),
776
+ navigationIntent: null,
777
+ filterExceptionId: null,
711
778
  };
712
779
  const selection = applyVisibleSessionSelection(state, nextSessions);
713
780
  return {
@@ -717,6 +784,39 @@ export function appReducer(state, action) {
717
784
  };
718
785
  }
719
786
 
787
+ case "sessions/navigationIntent": {
788
+ const sessionId = String(action.sessionId || "").trim();
789
+ if (!sessionId) return state;
790
+ const nextSessions = {
791
+ ...state.sessions,
792
+ navigationIntent: { sessionId, status: "pending" },
793
+ filterExceptionId: null,
794
+ };
795
+ const selection = applyVisibleSessionSelection(state, nextSessions);
796
+ return {
797
+ ...state,
798
+ sessions: selection.sessions,
799
+ ui: selection.ui,
800
+ };
801
+ }
802
+
803
+ case "sessions/navigationIntentFailed": {
804
+ const intent = state.sessions.navigationIntent;
805
+ const sessionId = String(action.sessionId || "").trim();
806
+ if (!intent || (sessionId && intent.sessionId !== sessionId)) return state;
807
+ return {
808
+ ...state,
809
+ sessions: {
810
+ ...state.sessions,
811
+ navigationIntent: {
812
+ sessionId: intent.sessionId,
813
+ status: "failed",
814
+ errorKind: action.errorKind === "not_found" ? "not_found" : "network",
815
+ },
816
+ },
817
+ };
818
+ }
819
+
720
820
  case "ui/modalSelection": {
721
821
  const modal = state.ui.modal;
722
822
  if (!modal) return state;
@@ -991,11 +1091,20 @@ export function appReducer(state, action) {
991
1091
  if (previousActiveId && previousActiveId !== action.sessionId) {
992
1092
  savedChatScroll[previousActiveId] = Number(state.ui.scroll?.chat) || 0;
993
1093
  }
1094
+ // Manual navigation to a different session releases the deep-link
1095
+ // latch and its transient filter exception.
1096
+ const releasesNavigationLatch = Boolean(
1097
+ state.sessions.navigationIntent
1098
+ && state.sessions.navigationIntent.sessionId !== action.sessionId,
1099
+ );
994
1100
  return {
995
1101
  ...state,
996
1102
  sessions: {
997
1103
  ...state.sessions,
998
1104
  activeSessionId: action.sessionId,
1105
+ ...(releasesNavigationLatch
1106
+ ? { navigationIntent: null, filterExceptionId: null }
1107
+ : {}),
999
1108
  },
1000
1109
  ui: {
1001
1110
  ...state.ui,
@@ -350,6 +350,10 @@ function matchesOwnerFilterDirect(session, ownerFilter = {}, auth = {}, ownerOve
350
350
  if (ownerKey === SYSTEM_OWNER_KEY) return ownerFilter.includeSystem === true;
351
351
  const currentUserKey = ownerKeyForOwner(auth?.principal);
352
352
  if (ownerFilter.includeMe && currentUserKey && ownerKey === currentUserKey) return true;
353
+ // "Shared with me": any non-system session owned by someone else. The
354
+ // catalog is viewer-scoped server-side, so a foreign owner implies the
355
+ // session was shared with (or is otherwise readable by) the viewer.
356
+ if (ownerFilter.includeShared === true && ownerKey !== currentUserKey) return true;
353
357
  return Array.isArray(ownerFilter.ownerKeys) && ownerFilter.ownerKeys.includes(ownerKey);
354
358
  }
355
359
 
@@ -728,6 +732,31 @@ function matchesSearchQuery(value, query) {
728
732
  return String(value || "").toLowerCase().includes(normalizedQuery);
729
733
  }
730
734
 
735
+ // The transient deep-link filter exception covers the linked session AND its
736
+ // ancestor chain (real parents plus the synthetic group:<id> row) so the
737
+ // linked row renders with its tree context instead of as an orphan.
738
+ function collectFilterExceptionIds(sessions) {
739
+ const exceptionId = sessions?.filterExceptionId || null;
740
+ const byId = sessions?.byId || {};
741
+ if (!exceptionId || !byId[exceptionId]) return null;
742
+ const ids = new Set([exceptionId]);
743
+ let current = byId[exceptionId];
744
+ let hops = 0;
745
+ while (current && hops < 16) {
746
+ let parentId = null;
747
+ if (current.parentSessionId && byId[current.parentSessionId]) {
748
+ parentId = current.parentSessionId;
749
+ } else if (!current.isGroup && current.groupId && byId[`group:${current.groupId}`]) {
750
+ parentId = `group:${current.groupId}`;
751
+ }
752
+ if (!parentId || ids.has(parentId)) break;
753
+ ids.add(parentId);
754
+ current = byId[parentId];
755
+ hops += 1;
756
+ }
757
+ return ids;
758
+ }
759
+
731
760
  export function selectSessionRows(state) {
732
761
  const totalDescendantCounts = getTotalDescendantCounts(state.sessions.byId);
733
762
  const visibleDescendantCounts = getVisibleDescendantCounts(state.sessions.flat, state.sessions.byId);
@@ -736,6 +765,7 @@ export function selectSessionRows(state) {
736
765
  const auth = state.auth || {};
737
766
  const pinnedSet = new Set(Array.isArray(state.sessions?.pinnedIds) ? state.sessions.pinnedIds : []);
738
767
  const selectedSet = new Set(Array.isArray(state.sessions?.selectedIds) ? state.sessions.selectedIds : []);
768
+ const filterExceptionIds = collectFilterExceptionIds(state.sessions);
739
769
 
740
770
  return state.sessions.flat.map((entry) => {
741
771
  const session = state.sessions.byId[entry.sessionId];
@@ -757,6 +787,7 @@ export function selectSessionRows(state) {
757
787
  canPin: canPinSessionRow(session),
758
788
  };
759
789
  }).filter((row) => {
790
+ if (filterExceptionIds?.has(row.sessionId)) return true;
760
791
  const session = state.sessions.byId[row.sessionId];
761
792
  if (!matchesOwnerFilter(session, ownerFilter, auth, state.sessions.byId)) return false;
762
793
  const effectiveOwner = effectiveSessionOwner(session, state.sessions.byId);
@@ -806,6 +837,39 @@ export function selectActiveSession(state) {
806
837
  return sessionId ? state.sessions.byId[sessionId] || null : null;
807
838
  }
808
839
 
840
+ export function selectNavigationIntent(state) {
841
+ return state.sessions?.navigationIntent || null;
842
+ }
843
+
844
+ /**
845
+ * Deep-link failure state for renderers. `not_found` deliberately covers both
846
+ * unknown and inaccessible sessions (no existence oracle); network/server
847
+ * failures keep a retryable flavor.
848
+ */
849
+ export function selectNavigationError(state) {
850
+ const intent = state.sessions?.navigationIntent;
851
+ if (!intent || intent.status !== "failed") return null;
852
+ const errorKind = intent.errorKind === "not_found" ? "not_found" : "network";
853
+ return {
854
+ sessionId: intent.sessionId,
855
+ errorKind,
856
+ retryable: errorKind === "network",
857
+ message: errorKind === "not_found"
858
+ ? "This session was not found or has not been shared with you."
859
+ : "Could not load the linked session. Check your connection and try again.",
860
+ };
861
+ }
862
+
863
+ /**
864
+ * Status copy for the transient deep-link filter exception — non-null while a
865
+ * linked session is being shown despite the current filters excluding it.
866
+ */
867
+ export function selectSessionFilterExceptionNotice(state) {
868
+ const exceptionId = state.sessions?.filterExceptionId || null;
869
+ if (!exceptionId || !state.sessions?.byId?.[exceptionId]) return null;
870
+ return "Showing linked session outside your current filters.";
871
+ }
872
+
809
873
  /**
810
874
  * True when the session row is actively running a turn that Stop can target.
811
875
  * Applies to user AND system sessions; group/container rows are not sessions.
@@ -1859,6 +1923,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
1859
1923
  width: Math.max(20, maxWidth),
1860
1924
  titleColor: USER_CHAT_COLOR,
1861
1925
  borderColor: USER_CHAT_COLOR,
1926
+ tableMode: options.tableMode,
1862
1927
  }),
1863
1928
  ...buildChatMessageLines({
1864
1929
  ...message,
@@ -1936,6 +2001,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
1936
2001
  width: Math.max(20, maxWidth),
1937
2002
  titleColor: message?.cardTitleColor || (message?.role === "system" ? "yellow" : "cyan"),
1938
2003
  borderColor: message?.cardBorderColor || "gray",
2004
+ tableMode: options.tableMode,
1939
2005
  ...(isSystemCard ? { bodyColor: "gray", fitToContent: true } : {}),
1940
2006
  });
1941
2007
  }
@@ -3714,7 +3780,7 @@ function buildNodeMapHeaderLine(nodeLabels, colWidth) {
3714
3780
  return runs;
3715
3781
  }
3716
3782
 
3717
- const SEQ_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "minimal", "none"]);
3783
+ const SEQ_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
3718
3784
 
3719
3785
  function shortModelForSequence(value) {
3720
3786
  const model = String(value || "").trim();
@@ -4022,10 +4088,12 @@ export function selectModelPickerModal(state, maxWidth = 72) {
4022
4088
  ? modal.items.findIndex((item) => item.id === model.id)
4023
4089
  : -1;
4024
4090
  const isSelected = itemIndex === selectedIndex;
4091
+ const isDisabled = Boolean(model.disabled);
4025
4092
  const labelRuns = fitRuns([
4026
4093
  { text: "· ", color: "gray" },
4027
- { text: model.modelName || model.qualifiedName || model.id, color: "white", bold: Boolean(model.isDefault) },
4094
+ { text: model.modelName || model.qualifiedName || model.id, color: isDisabled ? "gray" : "white", bold: Boolean(model.isDefault) && !isDisabled },
4028
4095
  ...(model.cost ? [{ text: ` [${model.cost}]`, color: "gray" }] : []),
4096
+ ...(isDisabled ? [{ text: " ⊘ needs GitHub key", color: "yellow" }] : []),
4029
4097
  ...(model.isDefault ? [{ text: " ← current default", color: "gray" }] : []),
4030
4098
  ], contentWidth);
4031
4099
 
@@ -4062,6 +4130,9 @@ export function selectModelPickerModal(state, maxWidth = 72) {
4062
4130
  ? [[{ text: `Reasoning: ${supportedReasoning.join(", ")}`, color: "gray" }]]
4063
4131
  : []),
4064
4132
  ...(defaultReasoning ? [[{ text: `Default reasoning: ${defaultReasoning}`, color: "gray" }]] : []),
4133
+ ...(selectedItem.disabled
4134
+ ? [[{ text: "Unavailable: requires a GitHub Copilot key. Add yours in the Admin console, then reopen this picker.", color: "yellow" }]]
4135
+ : []),
4065
4136
  [{ text: "", color: "gray" }],
4066
4137
  [{
4067
4138
  text: selectedItem.description || "No description available for this model.",
@@ -4234,11 +4305,12 @@ export function selectContextTierPickerModal(state, maxWidth = 64) {
4234
4305
 
4235
4306
  const selectedItem = items[selectedIndex] || null;
4236
4307
  const modelItem = modal.modelItem || null;
4308
+ const selectedLabel = selectedItem?.label || selectedItem?.id;
4237
4309
  const detailsLines = [
4238
4310
  [{ text: modelItem?.modelName || modelItem?.qualifiedName || "Selected model", color: "white", bold: true }],
4239
4311
  [{ text: `${modelItem?.providerId || "provider"} (${modelItem?.providerType || "provider"})`, color: "gray" }],
4240
4312
  [{ text: "", color: "gray" }],
4241
- [{ text: selectedItem?.id ? `Using context window: ${selectedItem.id}` : "Choose a context window.", color: "white" }],
4313
+ [{ text: selectedLabel ? `Using context window: ${selectedLabel}` : "Choose a context window.", color: "white" }],
4242
4314
  [{ text: "Long context costs more per token.", color: "gray" }],
4243
4315
  ];
4244
4316
 
@@ -4640,6 +4712,7 @@ function isOwnerFilterItemSelected(item, ownerFilter = {}, auth = {}) {
4640
4712
  if (item.kind === "system") return ownerFilter?.includeSystem === true;
4641
4713
  if (item.kind === "unowned") return ownerFilter?.includeUnowned === true;
4642
4714
  if (item.kind === "me") return ownerFilter?.includeMe === true && Boolean(ownerKeyForOwner(auth?.principal));
4715
+ if (item.kind === "shared") return ownerFilter?.includeShared === true;
4643
4716
  if (item.kind === "owner") return Array.isArray(ownerFilter?.ownerKeys) && ownerFilter.ownerKeys.includes(item.ownerKey);
4644
4717
  return false;
4645
4718
  }
@@ -13,6 +13,7 @@ export function normalizeSessionOwnerFilter(filter) {
13
13
  includeSystem: filter?.includeSystem === true,
14
14
  includeUnowned: filter?.includeUnowned === true,
15
15
  includeMe: filter?.includeMe === true,
16
+ includeShared: filter?.includeShared === true,
16
17
  ownerKeys,
17
18
  };
18
19
  }
@@ -180,6 +181,8 @@ export function createInitialState({ mode = "local", branding = null, themeId =
180
181
  filterQuery: "",
181
182
  ownerFilterExplicit: hasStoredSessionOwnerFilter,
182
183
  ownerFilter: normalizeSessionOwnerFilter(sessionOwnerFilter),
184
+ navigationIntent: null,
185
+ filterExceptionId: null,
183
186
  },
184
187
  history: {
185
188
  bySessionId: new Map(),