pilotswarm 0.5.46 → 0.5.48

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 (85) hide show
  1. package/mcp/README.md +1 -0
  2. package/mcp/dist/src/tools/providers.d.ts.map +1 -1
  3. package/mcp/dist/src/tools/providers.js +27 -3
  4. package/mcp/dist/src/tools/providers.js.map +1 -1
  5. package/package.json +2 -2
  6. package/tui/src/app.js +7 -11
  7. package/tui/src/node-sdk-transport.js +21 -0
  8. package/ui/core/src/commands.js +1 -0
  9. package/ui/core/src/controller.js +331 -68
  10. package/ui/core/src/history.js +5 -0
  11. package/ui/core/src/index.js +1 -0
  12. package/ui/core/src/reducer.js +93 -4
  13. package/ui/core/src/selectors.js +175 -15
  14. package/ui/core/src/state.js +31 -0
  15. package/ui/core/src/table-layout.js +155 -0
  16. package/ui/react/src/components.js +1 -0
  17. package/ui/react/src/web-app.js +1035 -323
  18. package/web/dist/assets/{abnfDiagram-VRR7QNED-GXM5pEfT.js → abnfDiagram-VRR7QNED-6rYy4Wj8.js} +1 -1
  19. package/web/dist/assets/{arc-D52-xt6J.js → arc-CrRZ1Za7.js} +1 -1
  20. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bxh49GyJ.js → architectureDiagram-ZJ3FMSHR-CPe2QuQL.js} +1 -1
  21. package/web/dist/assets/{blockDiagram-677ZJIJ3-D8LSE94K.js → blockDiagram-677ZJIJ3-DfT6vWOg.js} +1 -1
  22. package/web/dist/assets/{c4Diagram-LMCZKHZV-Dcs1NVIx.js → c4Diagram-LMCZKHZV-BO-VXBT8.js} +1 -1
  23. package/web/dist/assets/channel-HtOTPdHg.js +1 -0
  24. package/web/dist/assets/{chunk-2Q5K7J3B-Dy3zu7gI.js → chunk-2Q5K7J3B-nq8p0xwC.js} +1 -1
  25. package/web/dist/assets/{chunk-32BRIVSS-5v78VX3T.js → chunk-32BRIVSS-DstJjLIA.js} +1 -1
  26. package/web/dist/assets/{chunk-5VM5RSS4-DUJMCfdj.js → chunk-5VM5RSS4-Bq0MPH_Q.js} +1 -1
  27. package/web/dist/assets/{chunk-EX3LRPZG-DYyT1So9.js → chunk-EX3LRPZG-BPofXJWl.js} +1 -1
  28. package/web/dist/assets/{chunk-JWPE2WC7-Buzzx5R2.js → chunk-JWPE2WC7-BBr7v6mo.js} +1 -1
  29. package/web/dist/assets/{chunk-MOJQB5TN-B8fOWRD7.js → chunk-MOJQB5TN-DA7ca6Jr.js} +1 -1
  30. package/web/dist/assets/{chunk-RYQCIY6F-Bp04YO26.js → chunk-RYQCIY6F-fdXZe4nw.js} +1 -1
  31. package/web/dist/assets/{chunk-V7JOEXUC-Gw6h8gPE.js → chunk-V7JOEXUC-BSATEdk2.js} +1 -1
  32. package/web/dist/assets/{chunk-VR4S4FIN-ASf4dPE-.js → chunk-VR4S4FIN-RR27eQST.js} +1 -1
  33. package/web/dist/assets/{chunk-XXDRQBXY-C4B-MNNk.js → chunk-XXDRQBXY-Bh8VimxP.js} +1 -1
  34. package/web/dist/assets/classDiagram-OUVF2IWQ-C2P3dws0.js +1 -0
  35. package/web/dist/assets/classDiagram-v2-EOCWNBFH-C2P3dws0.js +1 -0
  36. package/web/dist/assets/{cose-bilkent-JH36ORCC-C0S415ey.js → cose-bilkent-JH36ORCC-eP239MQJ.js} +1 -1
  37. package/web/dist/assets/{cynefin-VYW2F7L2-LWUlL1XB.js → cynefin-VYW2F7L2-Sb8l4Vm6.js} +1 -1
  38. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-ClnNDuqy.js → cynefinDiagram-TSTJHNR4-BrWMkhw9.js} +1 -1
  39. package/web/dist/assets/{dagre-VKFMJZFB-CRMTDGPv.js → dagre-VKFMJZFB-DT5kNKEx.js} +1 -1
  40. package/web/dist/assets/{diagram-FQU43EPY-Cjt1nFI8.js → diagram-FQU43EPY-Dy0Bxyod.js} +1 -1
  41. package/web/dist/assets/{diagram-G47NLZAW-DynrWO-8.js → diagram-G47NLZAW-R_yWa2-6.js} +1 -1
  42. package/web/dist/assets/{diagram-NH7WQ7WH-CkEgxg4T.js → diagram-NH7WQ7WH-BV2cGmgX.js} +1 -1
  43. package/web/dist/assets/{diagram-OA4YK3LP-DzB43wzH.js → diagram-OA4YK3LP-auayiDmq.js} +1 -1
  44. package/web/dist/assets/{diagram-WEI45ONY-DqxH3-sW.js → diagram-WEI45ONY-CE7bHIYE.js} +1 -1
  45. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-5NxDrqIa.js → ebnfDiagram-CCIWWBDH-C3SMk6Uu.js} +1 -1
  46. package/web/dist/assets/{erDiagram-Q63AITRT-OfiKzPj4.js → erDiagram-Q63AITRT-CnTs6X2D.js} +1 -1
  47. package/web/dist/assets/{flowDiagram-23GEKE2U-DsqfPM3s.js → flowDiagram-23GEKE2U-DobYm7mq.js} +1 -1
  48. package/web/dist/assets/{ganttDiagram-NO4QXBWP-N48fLign.js → ganttDiagram-NO4QXBWP-AkTQtAsR.js} +1 -1
  49. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-rn1cXRYh.js → gitGraphDiagram-IHSO6WYX-CpLWsqgx.js} +1 -1
  50. package/web/dist/assets/{index-BHp6FirC.js → index-BCzNnCl0.js} +6 -6
  51. package/web/dist/assets/index-BUp5hFdX.css +1 -0
  52. package/web/dist/assets/{infoDiagram-FWYZ7A6U-CjJ3TLmy.js → infoDiagram-FWYZ7A6U-ByzTSgj_.js} +1 -1
  53. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-DvCylaIQ.js → ishikawaDiagram-FXEZZL3T-CaL9Wi8i.js} +1 -1
  54. package/web/dist/assets/{journeyDiagram-5HDEW3XC-B_Ethynx.js → journeyDiagram-5HDEW3XC-BillueZJ.js} +1 -1
  55. package/web/dist/assets/{kanban-definition-HUTT4EX6-BlQuhDqn.js → kanban-definition-HUTT4EX6-DI_IRVWQ.js} +1 -1
  56. package/web/dist/assets/{linear-DIwvxXrN.js → linear-Cabaq-RD.js} +1 -1
  57. package/web/dist/assets/{mermaid.core-CfAsl9Pc.js → mermaid.core-D8KnBjxP.js} +4 -4
  58. package/web/dist/assets/{mindmap-definition-LN4V7U3C-BxJep4HE.js → mindmap-definition-LN4V7U3C-Bl7FpPwk.js} +1 -1
  59. package/web/dist/assets/{papaparse.min-BjO79nbw.js → papaparse.min-CcjjKk0h.js} +1 -1
  60. package/web/dist/assets/{pegDiagram-2B236MQR-yceXoFi0.js → pegDiagram-2B236MQR-CmsG2owD.js} +1 -1
  61. package/web/dist/assets/{pieDiagram-ENE6RG2P-BNBzpT5B.js → pieDiagram-ENE6RG2P-CLA_mcHl.js} +1 -1
  62. package/web/dist/assets/pilotswarm-DMCBXi5A.js +97 -0
  63. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-DCg1DTax.js → quadrantDiagram-ABIIQ3AL-hT9bnnUG.js} +1 -1
  64. package/web/dist/assets/{railroadDiagram-RFXS5EU6-DQdy8xST.js → railroadDiagram-RFXS5EU6-CNC0kFW6.js} +1 -1
  65. package/web/dist/assets/{requirementDiagram-TGXJPOKE-N8PYSqS8.js → requirementDiagram-TGXJPOKE-C-nSthhK.js} +1 -1
  66. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DK9WhfTA.js → sankeyDiagram-HTMAVEWB-C5JdV5bI.js} +1 -1
  67. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DEpALkWi.js → sequenceDiagram-DBY2YBRQ-BMKBxKk9.js} +1 -1
  68. package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKOEPe9x.js → sizeCapture-X5ZJPWSS-CbwNw-TR.js} +1 -1
  69. package/web/dist/assets/{stateDiagram-2N3HPSRC-Dwwyy0hH.js → stateDiagram-2N3HPSRC-ChvGrUa_.js} +1 -1
  70. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DLbEwVA2.js +1 -0
  71. package/web/dist/assets/{swimlanes-5IMT3BWC-hDoJpKdQ.js → swimlanes-5IMT3BWC-D9Uzkbi7.js} +2 -2
  72. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DMzhdO6y.js +8 -0
  73. package/web/dist/assets/{timeline-definition-FHXFAJF6-CPXnQ7LH.js → timeline-definition-FHXFAJF6-ctFEfgUp.js} +1 -1
  74. package/web/dist/assets/{vennDiagram-L72KCM5P-CYsOwGTT.js → vennDiagram-L72KCM5P-DGrJhlyF.js} +1 -1
  75. package/web/dist/assets/{wardleyDiagram-EHGQE667-DKh__oEb.js → wardleyDiagram-EHGQE667-BGqtZ8dQ.js} +1 -1
  76. package/web/dist/assets/{xychartDiagram-FW5EYKEG-Bws7l8k9.js → xychartDiagram-FW5EYKEG-DAXXs9xC.js} +1 -1
  77. package/web/dist/index.html +3 -3
  78. package/web/runtime.js +22 -0
  79. package/web/dist/assets/channel-DE0-pG_Z.js +0 -1
  80. package/web/dist/assets/classDiagram-OUVF2IWQ-B0o6by6m.js +0 -1
  81. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B0o6by6m.js +0 -1
  82. package/web/dist/assets/index-DEymVbi5.css +0 -1
  83. package/web/dist/assets/pilotswarm-DewDyJhu.js +0 -97
  84. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CRKYt8KU.js +0 -1
  85. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DtnCdOQI.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
 
@@ -3051,6 +3171,8 @@ export class PilotSwarmUiController {
3051
3171
  * admin view) and refresh the user profile so the UI is current.
3052
3172
  */
3053
3173
  async openAdminConsole() {
3174
+ // One MODE at a time — see openBudget.
3175
+ if (this.getState().ui?.budgetOpen) this.closeBudget();
3054
3176
  this.dispatch({ type: "admin/visibility", visible: true });
3055
3177
  await this.refreshAdminProfile().catch(() => {});
3056
3178
  void this.refreshAdminModelProviders().catch(() => {});
@@ -3062,6 +3184,24 @@ export class PilotSwarmUiController {
3062
3184
  this.dispatch({ type: "admin/visibility", visible: false });
3063
3185
  }
3064
3186
 
3187
+ /**
3188
+ * The Workspace mode: sessions, chat and the panels. Closes whichever
3189
+ * other mode is up. A real destination, so the toolbar can offer "back
3190
+ * to the workspace" as a button rather than "close whatever is open".
3191
+ */
3192
+ openWorkspace() {
3193
+ if (this.getState().admin?.visible) this.closeAdminConsole();
3194
+ if (this.getState().ui?.budgetOpen) this.closeBudget();
3195
+ }
3196
+
3197
+ /** Which mode is on screen: "workspace" | "budget" | "admin". */
3198
+ currentMode() {
3199
+ const state = this.getState();
3200
+ if (state.admin?.visible) return "admin";
3201
+ if (state.ui?.budgetOpen) return "budget";
3202
+ return "workspace";
3203
+ }
3204
+
3065
3205
  /**
3066
3206
  * Land on a freshly created session. Selecting it is not enough: the
3067
3207
  * Admin Console REPLACES the workspace, so creating from there left the
@@ -3163,6 +3303,15 @@ export class PilotSwarmUiController {
3163
3303
  }
3164
3304
  }
3165
3305
 
3306
+ /**
3307
+ * Drop a failed provider mutation's error. Called when the sheet that
3308
+ * raised it closes — the failure was that sheet's, and a banner outliving
3309
+ * it describes a change the person already abandoned.
3310
+ */
3311
+ dismissAdminModelProviderMutationError() {
3312
+ this.dispatch({ type: "admin/modelProviders/mutationDismissed" });
3313
+ }
3314
+
3166
3315
  async _runAdminModelProviderMutation(pending, run) {
3167
3316
  this.dispatch({ type: "admin/modelProviders/mutationPending", pending });
3168
3317
  try {
@@ -3188,6 +3337,28 @@ export class PilotSwarmUiController {
3188
3337
  }));
3189
3338
  }
3190
3339
 
3340
+ /**
3341
+ * Rotate a provider's key in place.
3342
+ *
3343
+ * Two ops, one per scope, the same split deleteAdminProvider uses: a
3344
+ * personal provider answers to its owner, a shared one to an admin. Both
3345
+ * change ONLY the key — the name, type, base URL, defaults, system-session
3346
+ * routing and usage history all stay, which is the whole reason this
3347
+ * exists rather than delete-and-recreate.
3348
+ */
3349
+ async updateAdminProviderCredential({ name, credentials, shared = false } = {}) {
3350
+ const op = shared ? "updateSharedProviderCredential" : "updateMyProviderCredential";
3351
+ if (typeof this.transport[op] !== "function") {
3352
+ const error = shared
3353
+ ? "Shared provider credential updates are not available on this transport."
3354
+ : "Provider credential updates are not available on this transport.";
3355
+ this.dispatch({ type: "admin/modelProviders/mutationFailed", error });
3356
+ return { ok: false, result: null, error };
3357
+ }
3358
+ return this._runAdminModelProviderMutation("updateCredential", () =>
3359
+ this.transport[op]({ name, credentials }));
3360
+ }
3361
+
3191
3362
  async deleteAdminProvider(name, { shared = false } = {}) {
3192
3363
  const op = shared ? "deleteProvider" : "deleteMyProvider";
3193
3364
  if (typeof this.transport[op] !== "function") {
@@ -3265,13 +3436,25 @@ export class PilotSwarmUiController {
3265
3436
  name = `${base}-${suffix}`;
3266
3437
  suffix += 1;
3267
3438
  }
3268
- this.dispatch({ type: "admin/modelProviders/createBegin", name, typeId, shared });
3439
+ this.dispatch({ type: "admin/modelProviders/createBegin", name, typeId, shared, mode: "create" });
3269
3440
  }
3270
3441
 
3271
3442
  beginAdminCreateGithubProvider() {
3272
3443
  this.beginAdminCreateProvider({ shared: false });
3273
3444
  }
3274
3445
 
3446
+ beginAdminUpdateProviderCredential(provider) {
3447
+ if (!provider?.name || provider.class === "shared") return;
3448
+ this.dispatch({
3449
+ type: "admin/modelProviders/createBegin",
3450
+ name: provider.name,
3451
+ typeId: provider.typeId,
3452
+ shared: false,
3453
+ mode: "update",
3454
+ stage: "credential",
3455
+ });
3456
+ }
3457
+
3275
3458
  cycleAdminProviderCreateType() {
3276
3459
  const providers = selectAdminConsole(this.getState()).modelProviders;
3277
3460
  const create = this.getState().admin?.modelProviders?.create;
@@ -3342,7 +3525,9 @@ export class PilotSwarmUiController {
3342
3525
  };
3343
3526
  // Clear the credential from shared state before any network await.
3344
3527
  this.dispatch({ type: "admin/modelProviders/createSaving" });
3345
- const outcome = await this.createAdminProvider(input);
3528
+ const outcome = create.mode === "update"
3529
+ ? await this.updateAdminProviderCredential({ name: input.name, credentials: input.credentials })
3530
+ : await this.createAdminProvider(input);
3346
3531
  this.dispatch(outcome?.ok
3347
3532
  ? { type: "admin/modelProviders/createEnd" }
3348
3533
  : { type: "admin/modelProviders/createFailed", error: outcome?.error });
@@ -3929,6 +4114,10 @@ export class PilotSwarmUiController {
3929
4114
  * row it names is in it.
3930
4115
  */
3931
4116
  async openBudget({ provider = undefined } = {}) {
4117
+ // One MODE at a time: Workspace, Budget or the Admin Console. The two
4118
+ // flags used to be independent, so Budget could open behind an open
4119
+ // Admin Console and the toolbar showed both as active.
4120
+ if (this.getState().admin?.visible) this.closeAdminConsole();
3932
4121
  this.dispatch({
3933
4122
  type: "ui/budgetOpen",
3934
4123
  open: true,
@@ -3950,6 +4139,45 @@ export class PilotSwarmUiController {
3950
4139
  this.dispatch({ type: "ui/budgetOpen", open: false });
3951
4140
  }
3952
4141
 
4142
+ /** Which tab of the Budget screen: "providers" or "summary". */
4143
+ setBudgetTab(tab) {
4144
+ this.dispatch({ type: "budget/tab", tab });
4145
+ if (tab === "summary") this.loadUsageSummary().catch(() => {});
4146
+ }
4147
+
4148
+ /**
4149
+ * Change the Cluster summary's window or provider filter and re-read.
4150
+ * `providers` is the exact list to ask for (empty = all); `preset` is
4151
+ * what the picker shows ("all" | "shared" | "users" | "custom").
4152
+ */
4153
+ async setUsageSummaryFilter({ days, providers, preset } = {}) {
4154
+ this.dispatch({ type: "budget/summary/filter", days, providers, preset });
4155
+ await this.loadUsageSummary();
4156
+ }
4157
+
4158
+ /**
4159
+ * Read the Cluster summary for the current filter. One request; the
4160
+ * database builds the windows, the series and the model pivot over the
4161
+ * same rows, so the three can never disagree.
4162
+ */
4163
+ async loadUsageSummary() {
4164
+ if (typeof this.transport.getProviderUsageSummary !== "function") {
4165
+ this.dispatch({ type: "budget/summary/failed", error: "The cluster summary is not available on this transport." });
4166
+ return;
4167
+ }
4168
+ const filter = this.getState().budget?.summary || {};
4169
+ this.dispatch({ type: "budget/summary/loading" });
4170
+ try {
4171
+ const data = await this.transport.getProviderUsageSummary({
4172
+ days: filter.days || 14,
4173
+ ...(Array.isArray(filter.providers) && filter.providers.length ? { providers: filter.providers } : {}),
4174
+ });
4175
+ this.dispatch({ type: "budget/summary/loaded", data, fetchedAt: Date.now() });
4176
+ } catch (error) {
4177
+ this.dispatch({ type: "budget/summary/failed", error: budgetRefusalMessage(error) });
4178
+ }
4179
+ }
4180
+
3953
4181
  /**
3954
4182
  * The "show overall usage" tick.
3955
4183
  *
@@ -5648,6 +5876,10 @@ export class PilotSwarmUiController {
5648
5876
  try {
5649
5877
  const requestOptions = this.applyActiveGroupDefault(options);
5650
5878
  const created = await this.transport.createSessionForAgent(agentName, requestOptions);
5879
+ // Count the start for this person's "most used" ordering. Deliberately
5880
+ // not awaited: the picker's sort order must never delay opening the
5881
+ // session that was just created, and a lost count only reorders a list.
5882
+ this._recordAgentPickerUse(agentName);
5651
5883
  await this.placeCreatedSessionInGroup(created, requestOptions.groupId ?? null);
5652
5884
  await this.refreshSessions();
5653
5885
  await this.loadSession(created.sessionId);
@@ -6143,18 +6375,14 @@ export class PilotSwarmUiController {
6143
6375
  }
6144
6376
  : null;
6145
6377
 
6146
- // EVERYTHING starts closed. The dialog opens as a short menu of
6147
- // categories — Generic, then whichever of Built-in / Shared / Yours
6148
- // this deployment actually has — and you open the one you want. A
6149
- // section expanded by default is a section every other user has to
6150
- // scroll past.
6151
- const collapsed = [
6152
- AGENT_PICKER_BUILTIN_KEY,
6153
- AGENT_PICKER_SHARED_KEY,
6154
- AGENT_PICKER_MINE_KEY,
6155
- ...new Set(catalog.filter((item) => !item.builtin).map((item) => agentPickerPackageKey(item))),
6156
- ];
6157
- const items = buildAgentPickerItems(catalog, collapsed, genericItem);
6378
+ // Per-person start counts, so "most used" means most used BY YOU.
6379
+ // Best-effort: a profile that cannot be read just sorts alphabetically
6380
+ // rather than failing the picker open.
6381
+ const usage = await this._loadAgentPickerUsage();
6382
+ const sort = AGENT_PICKER_SORTS.includes(this._agentPickerSort)
6383
+ ? this._agentPickerSort
6384
+ : AGENT_PICKER_DEFAULT_SORT;
6385
+ const items = buildAgentPickerItems(catalog, { query: "", sort, usage, genericItem });
6158
6386
 
6159
6387
  this.dispatch({
6160
6388
  type: "ui/modal",
@@ -6163,8 +6391,10 @@ export class PilotSwarmUiController {
6163
6391
  title: "Select agent for new session",
6164
6392
  catalog,
6165
6393
  genericItem,
6166
- collapsed,
6167
6394
  items,
6395
+ query: "",
6396
+ sort,
6397
+ usage,
6168
6398
  // Land somewhere Enter does something useful. First choice is a
6169
6399
  // real agent row; with none visible (no generic, no baked
6170
6400
  // agents, every package closed) fall back to the first CLOSED
@@ -7715,61 +7945,96 @@ export class PilotSwarmUiController {
7715
7945
  * cursor shifts every index below it, and re-using the old number would
7716
7946
  * silently move the highlight onto a different agent.
7717
7947
  */
7718
- toggleAgentPickerSection(sectionKey, force = null) {
7948
+ /**
7949
+ * This person's per-agent start counts, from their profile settings.
7950
+ * Best-effort by design: a profile that cannot be read sorts the picker
7951
+ * alphabetically instead of refusing to open.
7952
+ */
7953
+ async _loadAgentPickerUsage() {
7954
+ try {
7955
+ if (typeof this.transport.getCurrentUserProfile !== "function") return null;
7956
+ const profile = await this.transport.getCurrentUserProfile();
7957
+ const raw = profile?.profileSettings?.[AGENT_USAGE_SETTING];
7958
+ if (!raw || typeof raw !== "object") return {};
7959
+ const usage = {};
7960
+ for (const [name, count] of Object.entries(raw)) {
7961
+ const n = Number(count);
7962
+ if (Number.isFinite(n) && n > 0) usage[name] = n;
7963
+ }
7964
+ return usage;
7965
+ } catch {
7966
+ return null;
7967
+ }
7968
+ }
7969
+
7970
+ /**
7971
+ * Count one start. Read-modify-write against the whole settings document,
7972
+ * because that is the shape the API takes — so a concurrent write from
7973
+ * another tab can lose a count. A miscount only reorders a picker, which
7974
+ * is not worth a locking protocol.
7975
+ */
7976
+ async _recordAgentPickerUse(agentName) {
7977
+ const name = String(agentName || "").trim();
7978
+ if (!name) return;
7979
+ try {
7980
+ if (typeof this.transport.getCurrentUserProfile !== "function"
7981
+ || typeof this.transport.setCurrentUserProfileSettings !== "function") return;
7982
+ const profile = await this.transport.getCurrentUserProfile();
7983
+ const settings = { ...(profile?.profileSettings || {}) };
7984
+ const usage = { ...(settings[AGENT_USAGE_SETTING] || {}) };
7985
+ usage[name] = (Number(usage[name]) || 0) + 1;
7986
+ settings[AGENT_USAGE_SETTING] = usage;
7987
+ // Into state before the network: the portal's own profile save
7988
+ // fires on the very activeSessionId change this create causes,
7989
+ // and it writes the whole document from state. If state does not
7990
+ // have the new count by then, that save erases it.
7991
+ this.dispatch({ type: "ui/agentPickerUsage", usage });
7992
+ await this.transport.setCurrentUserProfileSettings({ settings });
7993
+ } catch {
7994
+ // Never let bookkeeping fail a session create.
7995
+ }
7996
+ }
7997
+
7998
+ /** Re-run the list against a new query or sort, keeping the selection. */
7999
+ _refreshAgentPicker(patch) {
7719
8000
  const modal = this.getState().ui.modal;
7720
- if (!modal || modal.type !== "sessionAgentPicker" || !sectionKey) return;
7721
- const collapsed = new Set(modal.collapsed || []);
7722
- const shouldCollapse = force === null ? !collapsed.has(sectionKey) : force;
7723
- if (shouldCollapse === collapsed.has(sectionKey)) return;
7724
- if (shouldCollapse) collapsed.add(sectionKey);
7725
- else collapsed.delete(sectionKey);
7726
-
7727
- const nextCollapsed = [...collapsed];
7728
- const items = buildAgentPickerItems(modal.catalog || [], nextCollapsed, modal.genericItem || null);
8001
+ if (!modal || modal.type !== "sessionAgentPicker") return;
8002
+ const next = { ...modal, ...patch };
8003
+ const items = buildAgentPickerItems(next.catalog || [], {
8004
+ query: next.query,
8005
+ sort: next.sort,
8006
+ usage: next.usage,
8007
+ genericItem: next.genericItem || null,
8008
+ });
8009
+ // Keep the same agent selected when it survives the new list; land on
8010
+ // the first row when it does not, so Enter always does something.
7729
8011
  const selectedId = modal.items?.[modal.selectedIndex || 0]?.id;
7730
- let nextIndex = items.findIndex((item) => item.id === selectedId);
7731
- if (nextIndex < 0) {
7732
- // The selected row was inside the section that just closed — land
7733
- // on that section's own header, which is where it went.
7734
- nextIndex = Math.max(0, items.findIndex((item) => item.sectionKey === sectionKey));
7735
- }
8012
+ const keptIndex = items.findIndex((item) => item.id === selectedId);
7736
8013
  this.dispatch({
7737
8014
  type: "ui/modal",
7738
- modal: { ...modal, collapsed: nextCollapsed, items, selectedIndex: nextIndex },
8015
+ modal: { ...next, items, selectedIndex: keptIndex >= 0 ? keptIndex : 0 },
7739
8016
  });
7740
8017
  }
7741
8018
 
7742
- /**
7743
- * Left/right on the agent picker. Right opens a closed section (or steps
7744
- * into it); left closes an open one, or jumps to the parent header when
7745
- * the cursor is already on a leaf — the same shape as the session tree.
7746
- */
7747
- moveAgentPickerSection(delta) {
7748
- const modal = this.getState().ui.modal;
7749
- if (!modal || modal.type !== "sessionAgentPicker") return false;
7750
- const items = Array.isArray(modal.items) ? modal.items : [];
7751
- const index = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, items.length - 1));
7752
- const item = items[index];
7753
- if (!item) return false;
8019
+ setAgentPickerQuery(query) {
8020
+ this._refreshAgentPicker({ query: String(query ?? "") });
8021
+ }
7754
8022
 
7755
- if (delta > 0) {
7756
- if (item.kind === "section" && item.collapsed) {
7757
- this.toggleAgentPickerSection(item.sectionKey, false);
7758
- return true;
7759
- }
7760
- return false;
7761
- }
8023
+ setAgentPickerSort(sort) {
8024
+ if (!AGENT_PICKER_SORTS.includes(sort)) return;
8025
+ this._agentPickerSort = sort; // remembered for the next open
8026
+ this._refreshAgentPicker({ sort });
8027
+ }
7762
8028
 
7763
- if (item.kind === "section" && !item.collapsed) {
7764
- this.toggleAgentPickerSection(item.sectionKey, true);
7765
- return true;
7766
- }
7767
- for (let i = index - 1; i >= 0; i -= 1) {
7768
- if (items[i].kind === "section" && items[i].depth < item.depth) {
7769
- this.dispatch({ type: "ui/modalSelection", index: i });
7770
- return true;
7771
- }
7772
- }
8029
+ /**
8030
+ * Left/right on the agent picker: nothing.
8031
+ *
8032
+ * These keys used to open and close package headings. The picker is a flat
8033
+ * list of agents now, so there is nothing to expand — and returning false
8034
+ * matters, because that is what lets the keystroke reach the search box and
8035
+ * move the caret instead of being swallowed here.
8036
+ */
8037
+ moveAgentPickerSection() {
7773
8038
  return false;
7774
8039
  }
7775
8040
 
@@ -7992,11 +8257,6 @@ export class PilotSwarmUiController {
7992
8257
  }
7993
8258
  if (modal.type === "sessionAgentPicker") {
7994
8259
  const item = modal.items?.[modal.selectedIndex || 0];
7995
- // Enter on a heading opens or closes it; the dialog stays up.
7996
- if (item?.kind === "section") {
7997
- this.toggleAgentPickerSection(item.sectionKey);
7998
- return;
7999
- }
8000
8260
  // A called-only agent is rendered so the package's composition is
8001
8261
  // visible, but starting one cold is not a thing you can do. Refuse
8002
8262
  // here rather than letting the create fail after the dialog closes.
@@ -10205,6 +10465,9 @@ export class PilotSwarmUiController {
10205
10465
  case UI_COMMANDS.CLOSE_ADMIN_CONSOLE:
10206
10466
  this.closeAdminConsole();
10207
10467
  return;
10468
+ case UI_COMMANDS.OPEN_WORKSPACE:
10469
+ this.openWorkspace();
10470
+ return;
10208
10471
  case UI_COMMANDS.ADMIN_REFRESH_PROFILE:
10209
10472
  await this.refreshAdminProfile();
10210
10473
  return;
@@ -542,6 +542,11 @@ function buildChatMessage(event, role) {
542
542
  };
543
543
  }
544
544
 
545
+ // The Copilot SDK echoes the system prompt as a system.message on EVERY
546
+ // turn, and the worker keeps only a 120-char snippet of it (session-proxy.ts,
547
+ // "Full content omitted from CMS"). For one release (0.5.47) each echo also
548
+ // got a collapsed row in the chat pane — one per turn, and opening it showed
549
+ // the snippet, not the prompt. Activity feed only, as before.
545
550
  function shouldRenderSystemMessageAsActivity(event) {
546
551
  // Most system.message events stay out of the chat pane. They include the
547
552
  // per-turn system prompt sent to the LLM plus internal rehydration notices,
@@ -15,3 +15,4 @@ export * from "./controller.js";
15
15
  export { isThemeLight } from "./themes/helpers.js";
16
16
  export * from "./themes/index.js";
17
17
  export { validateCanvasAction, formatCanvasActionPrompt, parseCanvasActionContent, isCanvasActionContent, createCanvasActionLimiter, CANVAS_ACTION_PREFIX } from "./canvas-actions.js";
18
+ export * from "./table-layout.js";