pilotswarm 0.5.44 → 0.5.47

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 (98) hide show
  1. package/mcp/README.md +12 -0
  2. package/mcp/dist/src/context.d.ts.map +1 -1
  3. package/mcp/dist/src/context.js +9 -1
  4. package/mcp/dist/src/context.js.map +1 -1
  5. package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -1
  6. package/mcp/dist/src/tools/agent-packages.js +31 -0
  7. package/mcp/dist/src/tools/agent-packages.js.map +1 -1
  8. package/mcp/dist/src/tools/providers.d.ts.map +1 -1
  9. package/mcp/dist/src/tools/providers.js +15 -3
  10. package/mcp/dist/src/tools/providers.js.map +1 -1
  11. package/mcp/dist/src/util/respond.d.ts.map +1 -1
  12. package/mcp/dist/src/util/respond.js +6 -0
  13. package/mcp/dist/src/util/respond.js.map +1 -1
  14. package/package.json +2 -2
  15. package/tui/src/agents-cli.js +35 -0
  16. package/tui/src/app.js +7 -11
  17. package/tui/src/node-sdk-transport.js +70 -0
  18. package/ui/core/src/controller.js +313 -74
  19. package/ui/core/src/history.js +65 -10
  20. package/ui/core/src/reducer.js +51 -4
  21. package/ui/core/src/selectors.js +101 -20
  22. package/ui/core/src/state.js +11 -0
  23. package/ui/react/src/components.js +1 -0
  24. package/ui/react/src/web-app.js +938 -127
  25. package/web/api/canvas-plane.js +18 -6
  26. package/web/api/ws.js +22 -2
  27. package/web/auth/index.js +8 -1
  28. package/web/auth/normalize/entra.js +5 -1
  29. package/web/dist/assets/{abnfDiagram-VRR7QNED-67Q0WWge.js → abnfDiagram-VRR7QNED-VPL8m_9W.js} +1 -1
  30. package/web/dist/assets/{arc-BvcYFngS.js → arc-T5I3-c1B.js} +1 -1
  31. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CPBN0rk8.js → architectureDiagram-ZJ3FMSHR-DEP70bg6.js} +1 -1
  32. package/web/dist/assets/{blockDiagram-677ZJIJ3-D_hp1Oah.js → blockDiagram-677ZJIJ3-CvBu1MbO.js} +1 -1
  33. package/web/dist/assets/{c4Diagram-LMCZKHZV-DAOVGMIW.js → c4Diagram-LMCZKHZV-B5qwF4gm.js} +1 -1
  34. package/web/dist/assets/channel-D5LojeMw.js +1 -0
  35. package/web/dist/assets/{chunk-2Q5K7J3B-DajZuaot.js → chunk-2Q5K7J3B-zVW8dS7X.js} +1 -1
  36. package/web/dist/assets/{chunk-32BRIVSS-C6pXBsgt.js → chunk-32BRIVSS-DWctxZZS.js} +1 -1
  37. package/web/dist/assets/{chunk-5VM5RSS4-780edrmi.js → chunk-5VM5RSS4-Bq7_Vmf9.js} +1 -1
  38. package/web/dist/assets/{chunk-EX3LRPZG-C0e4NVuz.js → chunk-EX3LRPZG-B8CFB3v-.js} +1 -1
  39. package/web/dist/assets/{chunk-JWPE2WC7-uRe83I6C.js → chunk-JWPE2WC7-B7gLPjgZ.js} +1 -1
  40. package/web/dist/assets/{chunk-MOJQB5TN-B0WPoPjh.js → chunk-MOJQB5TN-B2x6q12r.js} +1 -1
  41. package/web/dist/assets/{chunk-RYQCIY6F-eRFuHHyG.js → chunk-RYQCIY6F-XorU9Stx.js} +1 -1
  42. package/web/dist/assets/{chunk-V7JOEXUC-C8FDXprk.js → chunk-V7JOEXUC-CsLa6pw5.js} +1 -1
  43. package/web/dist/assets/{chunk-VR4S4FIN-DPFpLQdB.js → chunk-VR4S4FIN-BBv9mwmj.js} +1 -1
  44. package/web/dist/assets/{chunk-XXDRQBXY-CoGQOPKW.js → chunk-XXDRQBXY-Cjm4mm6l.js} +1 -1
  45. package/web/dist/assets/classDiagram-OUVF2IWQ-C5edc5Or.js +1 -0
  46. package/web/dist/assets/classDiagram-v2-EOCWNBFH-C5edc5Or.js +1 -0
  47. package/web/dist/assets/{cose-bilkent-JH36ORCC-BTBSfO5v.js → cose-bilkent-JH36ORCC-Zw9ZQbVl.js} +1 -1
  48. package/web/dist/assets/{cynefin-VYW2F7L2-DgTkQEoK.js → cynefin-VYW2F7L2-BYnR9YLM.js} +1 -1
  49. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-DiBMlKoP.js → cynefinDiagram-TSTJHNR4-Cx8L0dOv.js} +1 -1
  50. package/web/dist/assets/{dagre-VKFMJZFB-Qv-4dcbk.js → dagre-VKFMJZFB-7GaqVcqz.js} +1 -1
  51. package/web/dist/assets/{diagram-FQU43EPY-CgyAb7yx.js → diagram-FQU43EPY-BWSRPJvU.js} +1 -1
  52. package/web/dist/assets/{diagram-G47NLZAW-BxN-KcGp.js → diagram-G47NLZAW-BELuAE1e.js} +1 -1
  53. package/web/dist/assets/{diagram-NH7WQ7WH-BiellpWz.js → diagram-NH7WQ7WH-DGXhPpri.js} +1 -1
  54. package/web/dist/assets/{diagram-OA4YK3LP-C8Ktgrwv.js → diagram-OA4YK3LP-DV8HqN0T.js} +1 -1
  55. package/web/dist/assets/{diagram-WEI45ONY-Bj5K1X91.js → diagram-WEI45ONY-Clv1X3ZL.js} +1 -1
  56. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DNQI2XT9.js → ebnfDiagram-CCIWWBDH-TpQx36oi.js} +1 -1
  57. package/web/dist/assets/{erDiagram-Q63AITRT-D5Xkllu6.js → erDiagram-Q63AITRT-DVVWd00c.js} +1 -1
  58. package/web/dist/assets/{flowDiagram-23GEKE2U-DJpTtMuj.js → flowDiagram-23GEKE2U-Cdz_8_Dw.js} +1 -1
  59. package/web/dist/assets/{ganttDiagram-NO4QXBWP-C-EQlCJf.js → ganttDiagram-NO4QXBWP-zlco-o7u.js} +1 -1
  60. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C8vpSbvW.js → gitGraphDiagram-IHSO6WYX-CfAZW1En.js} +1 -1
  61. package/web/dist/assets/index-CMtzFJre.css +1 -0
  62. package/web/dist/assets/index-tQXCQojP.js +24 -0
  63. package/web/dist/assets/{infoDiagram-FWYZ7A6U-CKNzsR3r.js → infoDiagram-FWYZ7A6U-0R-8efwI.js} +1 -1
  64. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CjyQq3lf.js → ishikawaDiagram-FXEZZL3T-lPpoXGlp.js} +1 -1
  65. package/web/dist/assets/{journeyDiagram-5HDEW3XC-CK3EEkJt.js → journeyDiagram-5HDEW3XC-D05vSVqu.js} +1 -1
  66. package/web/dist/assets/{kanban-definition-HUTT4EX6-CHQ8YkPx.js → kanban-definition-HUTT4EX6-ClaDdESv.js} +1 -1
  67. package/web/dist/assets/{linear-CUYirs1h.js → linear-CUnhHMvY.js} +1 -1
  68. package/web/dist/assets/{mermaid.core-Di8sQC4g.js → mermaid.core-DaYS-X8Y.js} +4 -4
  69. package/web/dist/assets/{mindmap-definition-LN4V7U3C-D38dmHP5.js → mindmap-definition-LN4V7U3C-N480VHds.js} +1 -1
  70. package/web/dist/assets/{papaparse.min-DWBU_pXS.js → papaparse.min-CNTcNgd8.js} +1 -1
  71. package/web/dist/assets/{pegDiagram-2B236MQR-BU759QCo.js → pegDiagram-2B236MQR-CVWyJ5ck.js} +1 -1
  72. package/web/dist/assets/{pieDiagram-ENE6RG2P-BbpfHbsj.js → pieDiagram-ENE6RG2P-UR9uohPf.js} +1 -1
  73. package/web/dist/assets/pilotswarm-BG6hk0oE.js +97 -0
  74. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BYZ3XHZn.js → quadrantDiagram-ABIIQ3AL-D2SIOetn.js} +1 -1
  75. package/web/dist/assets/{railroadDiagram-RFXS5EU6-D18gkZBJ.js → railroadDiagram-RFXS5EU6-OgN6qgOU.js} +1 -1
  76. package/web/dist/assets/{requirementDiagram-TGXJPOKE-D6vo26Ib.js → requirementDiagram-TGXJPOKE-BSUURBBz.js} +1 -1
  77. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CxQwXnRE.js → sankeyDiagram-HTMAVEWB-BBZhmSBZ.js} +1 -1
  78. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CfGisw4A.js → sequenceDiagram-DBY2YBRQ-C10IDNdW.js} +1 -1
  79. package/web/dist/assets/{sizeCapture-X5ZJPWSS-BDK7-yA-.js → sizeCapture-X5ZJPWSS-B9787MHa.js} +1 -1
  80. package/web/dist/assets/{stateDiagram-2N3HPSRC-8IyidYSV.js → stateDiagram-2N3HPSRC-BmSBo7es.js} +1 -1
  81. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BqpsfoJI.js +1 -0
  82. package/web/dist/assets/{swimlanes-5IMT3BWC-CgNqXCvM.js → swimlanes-5IMT3BWC-DOXpv_KQ.js} +2 -2
  83. package/web/dist/assets/swimlanesDiagram-G3AALYLV-k8WG_bWt.js +8 -0
  84. package/web/dist/assets/{timeline-definition-FHXFAJF6-DAJsGj6d.js → timeline-definition-FHXFAJF6-SjhGfPdI.js} +1 -1
  85. package/web/dist/assets/{vennDiagram-L72KCM5P-DDVdvG5k.js → vennDiagram-L72KCM5P-BiujYen6.js} +1 -1
  86. package/web/dist/assets/{wardleyDiagram-EHGQE667-CiAs_m72.js → wardleyDiagram-EHGQE667-6nLXBvwc.js} +1 -1
  87. package/web/dist/assets/{xychartDiagram-FW5EYKEG-D6OLJtsD.js → xychartDiagram-FW5EYKEG-Di7ioTvW.js} +1 -1
  88. package/web/dist/index.html +3 -3
  89. package/web/runtime.js +192 -6
  90. package/web/server.js +22 -0
  91. package/web/dist/assets/channel-DiW7lUT8.js +0 -1
  92. package/web/dist/assets/classDiagram-OUVF2IWQ-CpAhBd5x.js +0 -1
  93. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CpAhBd5x.js +0 -1
  94. package/web/dist/assets/index-C2ioXNJJ.js +0 -24
  95. package/web/dist/assets/index-CVYm02UI.css +0 -1
  96. package/web/dist/assets/pilotswarm-DrM3pXOV.js +0 -97
  97. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-F7qYkvpr.js +0 -1
  98. package/web/dist/assets/swimlanesDiagram-G3AALYLV-CUKrHLkY.js +0 -8
@@ -579,7 +579,7 @@ function buildSessionOwnerFilterItems(state) {
579
579
  id: "all",
580
580
  kind: "all",
581
581
  label: "All",
582
- description: "Show every session, including system and unowned sessions.",
582
+ description: "Every session you can see — your own, ones shared with you, and system sessions.",
583
583
  },
584
584
  {
585
585
  id: "system",
@@ -893,7 +893,127 @@ export function agentPickerInitialIndex(items = []) {
893
893
  return firstClosed >= 0 ? firstClosed : 0;
894
894
  }
895
895
 
896
- export function buildAgentPickerItems(catalog = [], collapsedKeys = [], genericItem = null) {
896
+ /** How the picker may be ordered. `used` is per-person; the rest are stable. */
897
+ export const AGENT_PICKER_SORTS = ["used", "name", "package"];
898
+ export const AGENT_PICKER_DEFAULT_SORT = "used";
899
+
900
+ /** Profile-settings key holding this person's per-agent start counts. */
901
+ export const AGENT_USAGE_SETTING = "agentPickerUsage";
902
+
903
+ /** Everything a query is matched against, lowercased once per agent. */
904
+ function agentHaystack(agent) {
905
+ return [
906
+ agent?.label, agent?.agentName, agent?.title, agent?.description,
907
+ agent?.packageTitle, agent?.packageName,
908
+ ...(Array.isArray(agent?.tools) ? agent.tools : []),
909
+ ].filter(Boolean).join(" ").toLowerCase();
910
+ }
911
+
912
+ /**
913
+ * Lexical match: every whitespace-separated term must appear somewhere in the
914
+ * agent's text. AND rather than OR, because "rcakit pg" should narrow to one
915
+ * agent rather than widen to everything mentioning either word.
916
+ *
917
+ * The score only ranks matches against each other — a name hit outranks a
918
+ * description hit, and a prefix outranks a mid-word hit, so typing "rca" puts
919
+ * the agents CALLED rcakit above the ones that merely mention it.
920
+ */
921
+ export function scoreAgentMatch(agent, query) {
922
+ const terms = String(query || "").toLowerCase().split(/\s+/).filter(Boolean);
923
+ if (terms.length === 0) return 0;
924
+ const hay = agentHaystack(agent);
925
+ const name = `${agent?.label || ""} ${agent?.agentName || ""}`.toLowerCase();
926
+ let score = 0;
927
+ for (const term of terms) {
928
+ if (!hay.includes(term)) return -1; // every term must land
929
+ if (name.startsWith(term)) score += 6;
930
+ else if (new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(name)) score += 4;
931
+ else if (name.includes(term)) score += 3;
932
+ else score += 1;
933
+ }
934
+ return score;
935
+ }
936
+
937
+ function agentUsageCount(usage, agent) {
938
+ if (!usage || !agent?.agentName) return 0;
939
+ return Number(usage[String(agent.agentName)]) || 0;
940
+ }
941
+
942
+ function compareAgentsForSort(sort, usage) {
943
+ const byName = (a, b) => String(a.label || a.agentName || "")
944
+ .localeCompare(String(b.label || b.agentName || ""), undefined, { sensitivity: "base" });
945
+ if (sort === "name") return byName;
946
+ if (sort === "package") {
947
+ return (a, b) => String(a.packageTitle || a.packageName || "")
948
+ .localeCompare(String(b.packageTitle || b.packageName || ""), undefined, { sensitivity: "base" })
949
+ || byName(a, b);
950
+ }
951
+ // "used": most-started first, then alphabetical so the long tail of
952
+ // never-started agents is not in arbitrary order.
953
+ return (a, b) => agentUsageCount(usage, b) - agentUsageCount(usage, a) || byName(a, b);
954
+ }
955
+
956
+ /**
957
+ * The picker list: ONE ROW PER AGENT.
958
+ *
959
+ * The old shape gave each package a header row and then repeated its agents
960
+ * beneath. Nearly every package holds exactly one agent, so that spent two
961
+ * rows saying the same thing twice and pushed the list past a screen. The
962
+ * package is still shown — as trailing metadata on the row it belongs to,
963
+ * and as a sort key — but it no longer costs a row of its own.
964
+ */
965
+ export function buildAgentPickerItems(catalog = [], opts = {}) {
966
+ const {
967
+ query = "",
968
+ sort = AGENT_PICKER_DEFAULT_SORT,
969
+ usage = null,
970
+ genericItem = null,
971
+ } = opts || {};
972
+
973
+ // Generic sits ABOVE everything and never filters out on an empty query:
974
+ // it is not an agent, it is the absence of one, and it is the most common
975
+ // pick. A typed query does filter it, so search can reach past it.
976
+ const items = [];
977
+ const terms = String(query || "").trim();
978
+ if (genericItem && (!terms || scoreAgentMatch({ label: genericItem.label, description: genericItem.description }, terms) >= 0)) {
979
+ items.push({ ...genericItem, depth: 0 });
980
+ }
981
+
982
+ // Composition still decides whether an agent can be STARTED — `startedBy`
983
+ // resolving inside its own set is what makes it called-only. The flat list
984
+ // drops the nesting, not that derivation, so each set still goes through
985
+ // orderAgentsByComposition; only its ordering is discarded here.
986
+ const raw = catalog.filter((agent) => agent && agent.kind !== "section");
987
+ const sets = new Map();
988
+ for (const agent of raw) {
989
+ const key = agent.builtin ? "builtin" : agentPickerPackageKey(agent);
990
+ if (!sets.has(key)) sets.set(key, []);
991
+ sets.get(key).push(agent);
992
+ }
993
+ const agents = [];
994
+ for (const set of sets.values()) {
995
+ for (const agent of orderAgentsByComposition(set)) agents.push({ ...agent, depth: 0 });
996
+ }
997
+
998
+ const scored = terms
999
+ ? agents.map((agent) => ({ agent, score: scoreAgentMatch(agent, terms) })).filter((row) => row.score >= 0)
1000
+ : agents.map((agent) => ({ agent, score: 0 }));
1001
+
1002
+ const compare = compareAgentsForSort(sort, usage);
1003
+ scored.sort((a, b) => (terms ? b.score - a.score : 0) || compare(a.agent, b.agent));
1004
+
1005
+ for (const { agent } of scored) {
1006
+ items.push({
1007
+ ...agent,
1008
+ depth: 0,
1009
+ usageCount: agentUsageCount(usage, agent),
1010
+ });
1011
+ }
1012
+ return items;
1013
+ }
1014
+
1015
+ /** @deprecated the grouped shape; kept only for the TUI's collapse tests. */
1016
+ export function buildGroupedAgentPickerItems(catalog = [], collapsedKeys = [], genericItem = null) {
897
1017
  const collapsed = new Set(collapsedKeys);
898
1018
  const items = [];
899
1019
 
@@ -2614,6 +2734,14 @@ export class PilotSwarmUiController {
2614
2734
  */
2615
2735
  async refreshWorkerRegistryIfStale() {
2616
2736
  const workers = this.getState().admin?.workers;
2737
+ // The registry is fleet:admin. Polling it as a plain user is a 403
2738
+ // every few seconds and a red console for nothing.
2739
+ const role = this.getState().auth?.authorization?.role;
2740
+ const isAdmin = role === "admin" || role === "anonymous" || this.getState().admin?.profile?.isAdmin === true;
2741
+ if (!isAdmin) {
2742
+ this.dispatch({ type: "admin/workers/attempt", skip: "not admin" });
2743
+ return;
2744
+ }
2617
2745
  if (workers?.loading) {
2618
2746
  this.dispatch({ type: "admin/workers/attempt", skip: "already in flight" });
2619
2747
  return;
@@ -3155,6 +3283,15 @@ export class PilotSwarmUiController {
3155
3283
  }
3156
3284
  }
3157
3285
 
3286
+ /**
3287
+ * Drop a failed provider mutation's error. Called when the sheet that
3288
+ * raised it closes — the failure was that sheet's, and a banner outliving
3289
+ * it describes a change the person already abandoned.
3290
+ */
3291
+ dismissAdminModelProviderMutationError() {
3292
+ this.dispatch({ type: "admin/modelProviders/mutationDismissed" });
3293
+ }
3294
+
3158
3295
  async _runAdminModelProviderMutation(pending, run) {
3159
3296
  this.dispatch({ type: "admin/modelProviders/mutationPending", pending });
3160
3297
  try {
@@ -3180,6 +3317,28 @@ export class PilotSwarmUiController {
3180
3317
  }));
3181
3318
  }
3182
3319
 
3320
+ /**
3321
+ * Rotate a provider's key in place.
3322
+ *
3323
+ * Two ops, one per scope, the same split deleteAdminProvider uses: a
3324
+ * personal provider answers to its owner, a shared one to an admin. Both
3325
+ * change ONLY the key — the name, type, base URL, defaults, system-session
3326
+ * routing and usage history all stay, which is the whole reason this
3327
+ * exists rather than delete-and-recreate.
3328
+ */
3329
+ async updateAdminProviderCredential({ name, credentials, shared = false } = {}) {
3330
+ const op = shared ? "updateSharedProviderCredential" : "updateMyProviderCredential";
3331
+ if (typeof this.transport[op] !== "function") {
3332
+ const error = shared
3333
+ ? "Shared provider credential updates are not available on this transport."
3334
+ : "Provider credential updates are not available on this transport.";
3335
+ this.dispatch({ type: "admin/modelProviders/mutationFailed", error });
3336
+ return { ok: false, result: null, error };
3337
+ }
3338
+ return this._runAdminModelProviderMutation("updateCredential", () =>
3339
+ this.transport[op]({ name, credentials }));
3340
+ }
3341
+
3183
3342
  async deleteAdminProvider(name, { shared = false } = {}) {
3184
3343
  const op = shared ? "deleteProvider" : "deleteMyProvider";
3185
3344
  if (typeof this.transport[op] !== "function") {
@@ -3257,13 +3416,25 @@ export class PilotSwarmUiController {
3257
3416
  name = `${base}-${suffix}`;
3258
3417
  suffix += 1;
3259
3418
  }
3260
- this.dispatch({ type: "admin/modelProviders/createBegin", name, typeId, shared });
3419
+ this.dispatch({ type: "admin/modelProviders/createBegin", name, typeId, shared, mode: "create" });
3261
3420
  }
3262
3421
 
3263
3422
  beginAdminCreateGithubProvider() {
3264
3423
  this.beginAdminCreateProvider({ shared: false });
3265
3424
  }
3266
3425
 
3426
+ beginAdminUpdateProviderCredential(provider) {
3427
+ if (!provider?.name || provider.class === "shared") return;
3428
+ this.dispatch({
3429
+ type: "admin/modelProviders/createBegin",
3430
+ name: provider.name,
3431
+ typeId: provider.typeId,
3432
+ shared: false,
3433
+ mode: "update",
3434
+ stage: "credential",
3435
+ });
3436
+ }
3437
+
3267
3438
  cycleAdminProviderCreateType() {
3268
3439
  const providers = selectAdminConsole(this.getState()).modelProviders;
3269
3440
  const create = this.getState().admin?.modelProviders?.create;
@@ -3334,7 +3505,9 @@ export class PilotSwarmUiController {
3334
3505
  };
3335
3506
  // Clear the credential from shared state before any network await.
3336
3507
  this.dispatch({ type: "admin/modelProviders/createSaving" });
3337
- const outcome = await this.createAdminProvider(input);
3508
+ const outcome = create.mode === "update"
3509
+ ? await this.updateAdminProviderCredential({ name: input.name, credentials: input.credentials })
3510
+ : await this.createAdminProvider(input);
3338
3511
  this.dispatch(outcome?.ok
3339
3512
  ? { type: "admin/modelProviders/createEnd" }
3340
3513
  : { type: "admin/modelProviders/createFailed", error: outcome?.error });
@@ -3654,6 +3827,13 @@ export class PilotSwarmUiController {
3654
3827
  case "delete":
3655
3828
  await this.transport.deleteAgentPackage(name, selector);
3656
3829
  break;
3830
+ // Editors live on the shared copy only — no selector.
3831
+ case "grantEditor":
3832
+ await this.transport.grantAgentPackageEditor(name, arg);
3833
+ break;
3834
+ case "revokeEditor":
3835
+ await this.transport.revokeAgentPackageEditor(name, arg);
3836
+ break;
3657
3837
  default:
3658
3838
  throw new Error(`unknown package action: ${kind}`);
3659
3839
  }
@@ -5270,12 +5450,11 @@ export class PilotSwarmUiController {
5270
5450
  // the authority; this is the friendly refusal). The canvas mutates —
5271
5451
  // a shared viewer may be looking at a different revision than the one
5272
5452
  // the creator is conversing through, so their clicks must not speak.
5273
- const me = this.getState().auth?.principal || null;
5274
- const ownerRow = this.getState().sessions.byId[sessionId]?.owner || null;
5275
- if (me && ownerRow && (String(ownerRow.provider || "") !== String(me.provider || "")
5276
- || String(ownerRow.subject || "") !== String(me.subject || ""))) {
5277
- return Promise.resolve({ ok: false, reason: "only the session's creator can use canvas controls" });
5278
- }
5453
+ // Who may ring is the SERVER's decision (owner, admin, or anyone with
5454
+ // session write — interactive-canvas-apps Part E). The client no
5455
+ // longer pre-refuses non-owners: a write-shared collaborator's click
5456
+ // must reach the agent, and the server's refusal for a read-only
5457
+ // viewer comes back as the result's reason.
5279
5458
  const contract = this.getState().canvas.bySessionId[sessionId]?.responseContract || null;
5280
5459
  const verdict = validateCanvasAction(contract, message);
5281
5460
  if (!verdict.ok) return Promise.resolve(verdict);
@@ -5386,6 +5565,23 @@ export class PilotSwarmUiController {
5386
5565
  return true;
5387
5566
  }
5388
5567
 
5568
+ /**
5569
+ * Listen for canvas KV changes on a session (the live path of the canvas
5570
+ * KV store). Returns an unsubscriber. Frames use this to post
5571
+ * `canvas-kv-change` into their page.
5572
+ */
5573
+ subscribeCanvasKv(sessionId, callback) {
5574
+ if (!sessionId || typeof callback !== "function") return () => {};
5575
+ if (!this._canvasKvListeners) this._canvasKvListeners = new Map();
5576
+ if (!this._canvasKvListeners.has(sessionId)) this._canvasKvListeners.set(sessionId, new Set());
5577
+ const set = this._canvasKvListeners.get(sessionId);
5578
+ set.add(callback);
5579
+ return () => {
5580
+ set.delete(callback);
5581
+ if (set.size === 0) this._canvasKvListeners.delete(sessionId);
5582
+ };
5583
+ }
5584
+
5389
5585
  applyCanvasDataEvent(sessionId, event) {
5390
5586
  this.dispatch({
5391
5587
  type: "canvas/data",
@@ -5413,6 +5609,17 @@ export class PilotSwarmUiController {
5413
5609
  if (event.eventType === "session.canvas_data") {
5414
5610
  this.applyCanvasDataEvent(sessionId, event);
5415
5611
  }
5612
+ if (event.eventType === "session.canvas_kv") {
5613
+ // KV changes never touch the reducer: they go straight to
5614
+ // the canvas frames listening for this session, which post
5615
+ // them into the page as `canvas-kv-change`.
5616
+ const listeners = this._canvasKvListeners?.get(sessionId);
5617
+ if (listeners) {
5618
+ for (const cb of listeners) {
5619
+ try { cb(event.data); } catch { /* one frame's problem */ }
5620
+ }
5621
+ }
5622
+ }
5416
5623
  if (event.eventType === "session.canvas_plane_released") {
5417
5624
  // The plane died for this session (server error/rollback).
5418
5625
  // Clear the reducer's takeover so legacy durable ticks — the
@@ -5606,6 +5813,10 @@ export class PilotSwarmUiController {
5606
5813
  try {
5607
5814
  const requestOptions = this.applyActiveGroupDefault(options);
5608
5815
  const created = await this.transport.createSessionForAgent(agentName, requestOptions);
5816
+ // Count the start for this person's "most used" ordering. Deliberately
5817
+ // not awaited: the picker's sort order must never delay opening the
5818
+ // session that was just created, and a lost count only reorders a list.
5819
+ this._recordAgentPickerUse(agentName);
5609
5820
  await this.placeCreatedSessionInGroup(created, requestOptions.groupId ?? null);
5610
5821
  await this.refreshSessions();
5611
5822
  await this.loadSession(created.sessionId);
@@ -6101,18 +6312,14 @@ export class PilotSwarmUiController {
6101
6312
  }
6102
6313
  : null;
6103
6314
 
6104
- // EVERYTHING starts closed. The dialog opens as a short menu of
6105
- // categories — Generic, then whichever of Built-in / Shared / Yours
6106
- // this deployment actually has — and you open the one you want. A
6107
- // section expanded by default is a section every other user has to
6108
- // scroll past.
6109
- const collapsed = [
6110
- AGENT_PICKER_BUILTIN_KEY,
6111
- AGENT_PICKER_SHARED_KEY,
6112
- AGENT_PICKER_MINE_KEY,
6113
- ...new Set(catalog.filter((item) => !item.builtin).map((item) => agentPickerPackageKey(item))),
6114
- ];
6115
- const items = buildAgentPickerItems(catalog, collapsed, genericItem);
6315
+ // Per-person start counts, so "most used" means most used BY YOU.
6316
+ // Best-effort: a profile that cannot be read just sorts alphabetically
6317
+ // rather than failing the picker open.
6318
+ const usage = await this._loadAgentPickerUsage();
6319
+ const sort = AGENT_PICKER_SORTS.includes(this._agentPickerSort)
6320
+ ? this._agentPickerSort
6321
+ : AGENT_PICKER_DEFAULT_SORT;
6322
+ const items = buildAgentPickerItems(catalog, { query: "", sort, usage, genericItem });
6116
6323
 
6117
6324
  this.dispatch({
6118
6325
  type: "ui/modal",
@@ -6121,8 +6328,10 @@ export class PilotSwarmUiController {
6121
6328
  title: "Select agent for new session",
6122
6329
  catalog,
6123
6330
  genericItem,
6124
- collapsed,
6125
6331
  items,
6332
+ query: "",
6333
+ sort,
6334
+ usage,
6126
6335
  // Land somewhere Enter does something useful. First choice is a
6127
6336
  // real agent row; with none visible (no generic, no baked
6128
6337
  // agents, every package closed) fall back to the first CLOSED
@@ -7673,61 +7882,96 @@ export class PilotSwarmUiController {
7673
7882
  * cursor shifts every index below it, and re-using the old number would
7674
7883
  * silently move the highlight onto a different agent.
7675
7884
  */
7676
- toggleAgentPickerSection(sectionKey, force = null) {
7885
+ /**
7886
+ * This person's per-agent start counts, from their profile settings.
7887
+ * Best-effort by design: a profile that cannot be read sorts the picker
7888
+ * alphabetically instead of refusing to open.
7889
+ */
7890
+ async _loadAgentPickerUsage() {
7891
+ try {
7892
+ if (typeof this.transport.getCurrentUserProfile !== "function") return null;
7893
+ const profile = await this.transport.getCurrentUserProfile();
7894
+ const raw = profile?.profileSettings?.[AGENT_USAGE_SETTING];
7895
+ if (!raw || typeof raw !== "object") return {};
7896
+ const usage = {};
7897
+ for (const [name, count] of Object.entries(raw)) {
7898
+ const n = Number(count);
7899
+ if (Number.isFinite(n) && n > 0) usage[name] = n;
7900
+ }
7901
+ return usage;
7902
+ } catch {
7903
+ return null;
7904
+ }
7905
+ }
7906
+
7907
+ /**
7908
+ * Count one start. Read-modify-write against the whole settings document,
7909
+ * because that is the shape the API takes — so a concurrent write from
7910
+ * another tab can lose a count. A miscount only reorders a picker, which
7911
+ * is not worth a locking protocol.
7912
+ */
7913
+ async _recordAgentPickerUse(agentName) {
7914
+ const name = String(agentName || "").trim();
7915
+ if (!name) return;
7916
+ try {
7917
+ if (typeof this.transport.getCurrentUserProfile !== "function"
7918
+ || typeof this.transport.setCurrentUserProfileSettings !== "function") return;
7919
+ const profile = await this.transport.getCurrentUserProfile();
7920
+ const settings = { ...(profile?.profileSettings || {}) };
7921
+ const usage = { ...(settings[AGENT_USAGE_SETTING] || {}) };
7922
+ usage[name] = (Number(usage[name]) || 0) + 1;
7923
+ settings[AGENT_USAGE_SETTING] = usage;
7924
+ // Into state before the network: the portal's own profile save
7925
+ // fires on the very activeSessionId change this create causes,
7926
+ // and it writes the whole document from state. If state does not
7927
+ // have the new count by then, that save erases it.
7928
+ this.dispatch({ type: "ui/agentPickerUsage", usage });
7929
+ await this.transport.setCurrentUserProfileSettings({ settings });
7930
+ } catch {
7931
+ // Never let bookkeeping fail a session create.
7932
+ }
7933
+ }
7934
+
7935
+ /** Re-run the list against a new query or sort, keeping the selection. */
7936
+ _refreshAgentPicker(patch) {
7677
7937
  const modal = this.getState().ui.modal;
7678
- if (!modal || modal.type !== "sessionAgentPicker" || !sectionKey) return;
7679
- const collapsed = new Set(modal.collapsed || []);
7680
- const shouldCollapse = force === null ? !collapsed.has(sectionKey) : force;
7681
- if (shouldCollapse === collapsed.has(sectionKey)) return;
7682
- if (shouldCollapse) collapsed.add(sectionKey);
7683
- else collapsed.delete(sectionKey);
7684
-
7685
- const nextCollapsed = [...collapsed];
7686
- const items = buildAgentPickerItems(modal.catalog || [], nextCollapsed, modal.genericItem || null);
7938
+ if (!modal || modal.type !== "sessionAgentPicker") return;
7939
+ const next = { ...modal, ...patch };
7940
+ const items = buildAgentPickerItems(next.catalog || [], {
7941
+ query: next.query,
7942
+ sort: next.sort,
7943
+ usage: next.usage,
7944
+ genericItem: next.genericItem || null,
7945
+ });
7946
+ // Keep the same agent selected when it survives the new list; land on
7947
+ // the first row when it does not, so Enter always does something.
7687
7948
  const selectedId = modal.items?.[modal.selectedIndex || 0]?.id;
7688
- let nextIndex = items.findIndex((item) => item.id === selectedId);
7689
- if (nextIndex < 0) {
7690
- // The selected row was inside the section that just closed — land
7691
- // on that section's own header, which is where it went.
7692
- nextIndex = Math.max(0, items.findIndex((item) => item.sectionKey === sectionKey));
7693
- }
7949
+ const keptIndex = items.findIndex((item) => item.id === selectedId);
7694
7950
  this.dispatch({
7695
7951
  type: "ui/modal",
7696
- modal: { ...modal, collapsed: nextCollapsed, items, selectedIndex: nextIndex },
7952
+ modal: { ...next, items, selectedIndex: keptIndex >= 0 ? keptIndex : 0 },
7697
7953
  });
7698
7954
  }
7699
7955
 
7700
- /**
7701
- * Left/right on the agent picker. Right opens a closed section (or steps
7702
- * into it); left closes an open one, or jumps to the parent header when
7703
- * the cursor is already on a leaf — the same shape as the session tree.
7704
- */
7705
- moveAgentPickerSection(delta) {
7706
- const modal = this.getState().ui.modal;
7707
- if (!modal || modal.type !== "sessionAgentPicker") return false;
7708
- const items = Array.isArray(modal.items) ? modal.items : [];
7709
- const index = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, items.length - 1));
7710
- const item = items[index];
7711
- if (!item) return false;
7956
+ setAgentPickerQuery(query) {
7957
+ this._refreshAgentPicker({ query: String(query ?? "") });
7958
+ }
7712
7959
 
7713
- if (delta > 0) {
7714
- if (item.kind === "section" && item.collapsed) {
7715
- this.toggleAgentPickerSection(item.sectionKey, false);
7716
- return true;
7717
- }
7718
- return false;
7719
- }
7960
+ setAgentPickerSort(sort) {
7961
+ if (!AGENT_PICKER_SORTS.includes(sort)) return;
7962
+ this._agentPickerSort = sort; // remembered for the next open
7963
+ this._refreshAgentPicker({ sort });
7964
+ }
7720
7965
 
7721
- if (item.kind === "section" && !item.collapsed) {
7722
- this.toggleAgentPickerSection(item.sectionKey, true);
7723
- return true;
7724
- }
7725
- for (let i = index - 1; i >= 0; i -= 1) {
7726
- if (items[i].kind === "section" && items[i].depth < item.depth) {
7727
- this.dispatch({ type: "ui/modalSelection", index: i });
7728
- return true;
7729
- }
7730
- }
7966
+ /**
7967
+ * Left/right on the agent picker: nothing.
7968
+ *
7969
+ * These keys used to open and close package headings. The picker is a flat
7970
+ * list of agents now, so there is nothing to expand — and returning false
7971
+ * matters, because that is what lets the keystroke reach the search box and
7972
+ * move the caret instead of being swallowed here.
7973
+ */
7974
+ moveAgentPickerSection() {
7731
7975
  return false;
7732
7976
  }
7733
7977
 
@@ -7950,11 +8194,6 @@ export class PilotSwarmUiController {
7950
8194
  }
7951
8195
  if (modal.type === "sessionAgentPicker") {
7952
8196
  const item = modal.items?.[modal.selectedIndex || 0];
7953
- // Enter on a heading opens or closes it; the dialog stays up.
7954
- if (item?.kind === "section") {
7955
- this.toggleAgentPickerSection(item.sectionKey);
7956
- return;
7957
- }
7958
8197
  // A called-only agent is rendered so the package's composition is
7959
8198
  // visible, but starting one cold is not a thing you can do. Refuse
7960
8199
  // here rather than letting the create fail after the dialog closes.
@@ -129,6 +129,18 @@ export function stripLeadingRehydrationNoticeText(text) {
129
129
  return normalized;
130
130
  }
131
131
 
132
+ // The runtime neutralizes a FORGED `[SYSTEM:` marker by inserting a zero-width
133
+ // character after the bracket, so a collaborator cannot inject system guidance
134
+ // (orchestration/utils.ts). That neutralizer runs when the queued message is
135
+ // consumed — AFTER the runtime has appended its own timer/cron notice to the
136
+ // same text — so a GENUINE notice on a message from anyone but the session
137
+ // owner arrives neutralized too. Match both spellings, or those notices print
138
+ // as raw prose in the transcript.
139
+ const NOTICE_MARKER_INVISIBLES = "[\\u200b-\\u200f\\u2060-\\u2064\\ufeff]*";
140
+ const SYSTEM_NOTICE_LINE_RE = new RegExp(`^\\s*\\[${NOTICE_MARKER_INVISIBLES}SYSTEM:`, "i");
141
+ const SYSTEM_NOTICE_SINGLE_LINE_RE = new RegExp(`^\\s*\\[${NOTICE_MARKER_INVISIBLES}SYSTEM:\\s*(.*?)\\]\\s*$`, "i");
142
+ const SYSTEM_NOTICE_OPENING_RE = new RegExp(`^\\s*\\[${NOTICE_MARKER_INVISIBLES}SYSTEM:\\s*`, "i");
143
+
132
144
  function splitSystemNoticeSegments(text) {
133
145
  const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
134
146
  const segments = [];
@@ -145,13 +157,13 @@ function splitSystemNoticeSegments(text) {
145
157
 
146
158
  for (let index = 0; index < lines.length;) {
147
159
  const line = lines[index];
148
- if (!/^\s*\[SYSTEM:/i.test(line)) {
160
+ if (!SYSTEM_NOTICE_LINE_RE.test(line)) {
149
161
  textLines.push(line);
150
162
  index += 1;
151
163
  continue;
152
164
  }
153
165
 
154
- const singleLineMatch = /^\s*\[SYSTEM:\s*(.*?)\]\s*$/i.exec(line);
166
+ const singleLineMatch = SYSTEM_NOTICE_SINGLE_LINE_RE.exec(line);
155
167
  if (singleLineMatch) {
156
168
  flushText();
157
169
  segments.push({
@@ -162,7 +174,7 @@ function splitSystemNoticeSegments(text) {
162
174
  continue;
163
175
  }
164
176
 
165
- const noticeLines = [line.replace(/^\s*\[SYSTEM:\s*/i, "")];
177
+ const noticeLines = [line.replace(SYSTEM_NOTICE_OPENING_RE, "")];
166
178
  let closingIndex = -1;
167
179
  for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
168
180
  const closingLine = lines[cursor];
@@ -254,7 +266,7 @@ function isInternalSystemLikeText(text) {
254
266
  const normalized = normalizeMessageText(text);
255
267
  if (!normalized) return false;
256
268
 
257
- return /^\[SYSTEM:/i.test(normalized)
269
+ return SYSTEM_NOTICE_LINE_RE.test(normalized)
258
270
  || /^\[CHILD_UPDATE\b/i.test(normalized)
259
271
  || /^\[SESSION_MESSAGE(?:_RESPONSE)?\b/i.test(normalized)
260
272
  || /^Buffered child updates arrived /i.test(normalized)
@@ -469,13 +481,19 @@ function buildChatMessage(event, role) {
469
481
  const sessionMessageCard = buildSessionMessageChatCard(event, rawText);
470
482
  if (sessionMessageCard) return sessionMessageCard;
471
483
 
484
+ // The attribution prefix and any embedded system notice come off BEFORE
485
+ // the canvas check. A collaborator pressing a button in the drawn page
486
+ // arrives as `[FROM: …]\n[canvas-action] {…}\n\n[SYSTEM: …]`: testing the
487
+ // raw text misses it on both ends, and the message then prints as an
488
+ // ordinary chat line with its raw JSON payload showing.
489
+ const visibleText = stripLeadingSenderMarker(extractVisibleChatText(rawText, role));
490
+
472
491
  // A structured canvas response — the browser sent it on the viewer's
473
492
  // behalf after validating it against the drawn contract. One item shape
474
493
  // from ONE builder, so bulk load and live append cannot disagree. The
475
- // portal chat hides it (the redraw is the visible half of the loop);
476
- // the TUI shows a compact line.
477
- if (role === "user" && isCanvasActionContent(rawText)) {
478
- const parsed = parseCanvasActionContent(rawText);
494
+ // portal collapses it to one row; the TUI shows a compact line.
495
+ if (role === "user" && isCanvasActionContent(visibleText)) {
496
+ const parsed = parseCanvasActionContent(visibleText);
479
497
  if (parsed) {
480
498
  return {
481
499
  id: `${event.sessionId}:${event.seq}`,
@@ -483,7 +501,7 @@ function buildChatMessage(event, role) {
483
501
  role: "user",
484
502
  action: parsed.action,
485
503
  data: parsed.data,
486
- text: rawText,
504
+ text: visibleText,
487
505
  time: formatTimestamp(event.createdAt),
488
506
  createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
489
507
  // Without the ids, the distinct-ids rule can never fire and two
@@ -496,7 +514,7 @@ function buildChatMessage(event, role) {
496
514
  }
497
515
  }
498
516
 
499
- const text = stripLeadingSenderMarker(extractVisibleChatText(rawText, role));
517
+ const text = visibleText;
500
518
  if (!hasVisibleMessageText(text)) return null;
501
519
  const clientMessageIds = Array.isArray(event?.data?.clientMessageIds)
502
520
  ? event.data.clientMessageIds.filter((id) => typeof id === "string" && id)
@@ -524,6 +542,27 @@ function buildChatMessage(event, role) {
524
542
  };
525
543
  }
526
544
 
545
+ // The per-turn system prompt, as the SDK summarises it for CMS: a marker line
546
+ // naming the size with a 120-char snippet, never the whole 170k-char prompt.
547
+ // Matched narrowly on purpose — it is the ONE system.message worth a row in
548
+ // the conversation, and widening this would move rehydration notices, child
549
+ // updates and sub-agent chatter in with it.
550
+ const SYSTEM_PROMPT_NOTICE_RE = /rebuilt the full system prompt for model input/i;
551
+
552
+ /**
553
+ * Is this the system prompt itself, rather than internal chatter?
554
+ *
555
+ * It stays in the ACTIVITY feed as before — that is the raw diagnostic
556
+ * stream — and additionally earns one collapsed row in the transcript, the
557
+ * same affordance as a sub-agent response or a canvas action. Visible enough
558
+ * to open when you want to know what the model was told; folded away the rest
559
+ * of the time.
560
+ */
561
+ export function isSystemPromptNotice(event) {
562
+ if (event?.eventType !== "system.message") return false;
563
+ return SYSTEM_PROMPT_NOTICE_RE.test(messageTextFromEvent(event) || "");
564
+ }
565
+
527
566
  function shouldRenderSystemMessageAsActivity(event) {
528
567
  // Most system.message events stay out of the chat pane. They include the
529
568
  // per-turn system prompt sent to the LLM plus internal rehydration notices,
@@ -1102,6 +1141,13 @@ export function buildHistoryModel(events = [], options = {}) {
1102
1141
  if (shouldRenderSystemMessageAsActivity(event)) {
1103
1142
  const activityItem = formatActivity(event);
1104
1143
  if (activityItem) activity.push(activityItem);
1144
+ // The system prompt also earns a collapsed row in the
1145
+ // transcript. It stays in the activity feed above — that is
1146
+ // the raw stream — and this is the readable handle on it.
1147
+ if (isSystemPromptNotice(event)) {
1148
+ const message = buildChatMessage(event, "system");
1149
+ if (message) chat.push(message);
1150
+ }
1105
1151
  } else {
1106
1152
  const message = buildChatMessage(event, "system");
1107
1153
  if (message) chat.push(message);
@@ -1229,6 +1275,15 @@ export function appendEventToHistory(history, event) {
1229
1275
  next.activity.push(activityItem);
1230
1276
  next.activity = clampHistoryItems(next.activity, loadedEventLimit);
1231
1277
  }
1278
+ // Same as the bulk path: the system prompt is also worth one
1279
+ // collapsed row in the conversation.
1280
+ if (isSystemPromptNotice(event)) {
1281
+ const message = buildChatMessage(event, "system");
1282
+ if (message) {
1283
+ next.chat.push(message);
1284
+ next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
1285
+ }
1286
+ }
1232
1287
  } else {
1233
1288
  const message = buildChatMessage(event, "system");
1234
1289
  if (!message) return next;