pilotswarm 0.5.32 → 0.5.34

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/package.json +2 -2
  2. package/tui/src/app.js +10 -0
  3. package/tui/src/node-sdk-transport.js +11 -0
  4. package/ui/core/src/controller.js +381 -12
  5. package/ui/core/src/reducer.js +125 -28
  6. package/ui/core/src/selectors.js +158 -268
  7. package/ui/core/src/state.js +6 -1
  8. package/ui/core/src/themes/catppuccin-mocha.js +2 -1
  9. package/ui/core/src/themes/cobalt2.js +2 -1
  10. package/ui/core/src/themes/doom.js +2 -1
  11. package/ui/core/src/themes/dracula.js +2 -1
  12. package/ui/core/src/themes/github-dark.js +2 -1
  13. package/ui/core/src/themes/github-light.js +2 -1
  14. package/ui/core/src/themes/gruvbox-dark.js +2 -1
  15. package/ui/core/src/themes/hacker-x-matrix.js +2 -1
  16. package/ui/core/src/themes/hacker-x-orion-prime.js +2 -1
  17. package/ui/core/src/themes/helpers.js +22 -4
  18. package/ui/core/src/themes/high-contrast-mono.js +2 -1
  19. package/ui/core/src/themes/index.js +24 -5
  20. package/ui/core/src/themes/ms-dos.js +2 -1
  21. package/ui/core/src/themes/noctis-obscuro.js +2 -1
  22. package/ui/core/src/themes/noctis.js +2 -1
  23. package/ui/core/src/themes/rust.js +125 -0
  24. package/ui/core/src/themes/spongebob.js +110 -0
  25. package/ui/core/src/themes/terminal-green.js +2 -1
  26. package/ui/core/src/themes/win95.js +2 -1
  27. package/ui/core/src/themes/winamp.js +2 -1
  28. package/ui/core/src/themes/workspace-dark.js +2 -1
  29. package/ui/react/src/web-app.js +240 -266
  30. package/web/dist/assets/{abnfDiagram-VRR7QNED-mXFQ4a3c.js → abnfDiagram-VRR7QNED-BFOimVWK.js} +1 -1
  31. package/web/dist/assets/{arc-BPuuvbge.js → arc-uUHjvwEo.js} +1 -1
  32. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Dt4uUYIe.js → architectureDiagram-ZJ3FMSHR-CxeX_vea.js} +1 -1
  33. package/web/dist/assets/{blockDiagram-677ZJIJ3-Ax8Q22vV.js → blockDiagram-677ZJIJ3-Cno8kltG.js} +1 -1
  34. package/web/dist/assets/{c4Diagram-LMCZKHZV-CPiukyHt.js → c4Diagram-LMCZKHZV-viONVybf.js} +1 -1
  35. package/web/dist/assets/channel-IURmSsjn.js +1 -0
  36. package/web/dist/assets/{chunk-2Q5K7J3B-VJLqCZJX.js → chunk-2Q5K7J3B-CKfICIGD.js} +1 -1
  37. package/web/dist/assets/{chunk-32BRIVSS-DqOshxhf.js → chunk-32BRIVSS-CxmwEY56.js} +1 -1
  38. package/web/dist/assets/{chunk-5VM5RSS4-KPqV21jV.js → chunk-5VM5RSS4-_eVYLGll.js} +1 -1
  39. package/web/dist/assets/{chunk-EX3LRPZG-DfhiCVl_.js → chunk-EX3LRPZG-C4aqbjrf.js} +1 -1
  40. package/web/dist/assets/{chunk-JWPE2WC7-H6khJPuW.js → chunk-JWPE2WC7-Bh8ZxrI8.js} +1 -1
  41. package/web/dist/assets/{chunk-MOJQB5TN-BKMAzQxj.js → chunk-MOJQB5TN-BXgLlWVP.js} +1 -1
  42. package/web/dist/assets/{chunk-RYQCIY6F-DNXRTxtj.js → chunk-RYQCIY6F-B1rMIdfF.js} +1 -1
  43. package/web/dist/assets/{chunk-V7JOEXUC-B56PM6Gv.js → chunk-V7JOEXUC-C3AnSHXk.js} +1 -1
  44. package/web/dist/assets/{chunk-VR4S4FIN-Boi3hGkB.js → chunk-VR4S4FIN-CeoMJIBs.js} +1 -1
  45. package/web/dist/assets/{chunk-XXDRQBXY-DmJyn890.js → chunk-XXDRQBXY-DzaxQ9iP.js} +1 -1
  46. package/web/dist/assets/classDiagram-OUVF2IWQ-FsfpM3iu.js +1 -0
  47. package/web/dist/assets/classDiagram-v2-EOCWNBFH-FsfpM3iu.js +1 -0
  48. package/web/dist/assets/{cose-bilkent-JH36ORCC-BRdFlFWt.js → cose-bilkent-JH36ORCC-G46fvUOx.js} +1 -1
  49. package/web/dist/assets/{cynefin-VYW2F7L2-DqE-xib2.js → cynefin-VYW2F7L2-DXwqVhWA.js} +1 -1
  50. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-D-QBZjIL.js → cynefinDiagram-TSTJHNR4-DNNFmaXb.js} +1 -1
  51. package/web/dist/assets/{dagre-VKFMJZFB-DYu9oQV6.js → dagre-VKFMJZFB-BCfh-h3o.js} +1 -1
  52. package/web/dist/assets/{diagram-FQU43EPY-DOwYccCg.js → diagram-FQU43EPY-CyG9b6bj.js} +1 -1
  53. package/web/dist/assets/{diagram-G47NLZAW-DTSEmrPr.js → diagram-G47NLZAW-DdxDahK3.js} +1 -1
  54. package/web/dist/assets/{diagram-NH7WQ7WH-OqbzzquA.js → diagram-NH7WQ7WH-CjrZ_eja.js} +1 -1
  55. package/web/dist/assets/{diagram-OA4YK3LP-BMVG6A8I.js → diagram-OA4YK3LP-5NAjQpk2.js} +1 -1
  56. package/web/dist/assets/{diagram-WEI45ONY-Rn2Q5xaz.js → diagram-WEI45ONY-fLuDb54-.js} +1 -1
  57. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-aL32_v5y.js → ebnfDiagram-CCIWWBDH-DL9qnZpa.js} +1 -1
  58. package/web/dist/assets/{erDiagram-Q63AITRT-DKCpyczx.js → erDiagram-Q63AITRT-C_ynfrWY.js} +1 -1
  59. package/web/dist/assets/{flowDiagram-23GEKE2U-CrZTPgR_.js → flowDiagram-23GEKE2U-dYuH8HYv.js} +1 -1
  60. package/web/dist/assets/{ganttDiagram-NO4QXBWP-DnSE1OtC.js → ganttDiagram-NO4QXBWP-e8Meh3f3.js} +1 -1
  61. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C5SeYPUR.js → gitGraphDiagram-IHSO6WYX-D_tZ4VL9.js} +1 -1
  62. package/web/dist/assets/index-Ct7SQv1M.css +1 -0
  63. package/web/dist/assets/{index-B-TEKrRB.js → index-DcQes9h8.js} +6 -6
  64. package/web/dist/assets/{infoDiagram-FWYZ7A6U-DDnJZp0X.js → infoDiagram-FWYZ7A6U-Dffhqp73.js} +1 -1
  65. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-ewhzfFkq.js → ishikawaDiagram-FXEZZL3T-DYt9H7kU.js} +1 -1
  66. package/web/dist/assets/{journeyDiagram-5HDEW3XC-T0iWWkLM.js → journeyDiagram-5HDEW3XC-Qv1vVu0F.js} +1 -1
  67. package/web/dist/assets/{kanban-definition-HUTT4EX6-D1MFQZjb.js → kanban-definition-HUTT4EX6-CTAUo40G.js} +1 -1
  68. package/web/dist/assets/{linear-BKfPK9wE.js → linear-CqLsWFLj.js} +1 -1
  69. package/web/dist/assets/{mermaid.core-CyzD7dTc.js → mermaid.core-okRCGPQQ.js} +4 -4
  70. package/web/dist/assets/{mindmap-definition-LN4V7U3C-CWXtcTlP.js → mindmap-definition-LN4V7U3C-DVOOrIwD.js} +1 -1
  71. package/web/dist/assets/{papaparse.min-DLUoV5k5.js → papaparse.min-Ddflu8oC.js} +1 -1
  72. package/web/dist/assets/{pegDiagram-2B236MQR-BFjZun-r.js → pegDiagram-2B236MQR-BLJlC0MZ.js} +1 -1
  73. package/web/dist/assets/{pieDiagram-ENE6RG2P-DMDtW4g7.js → pieDiagram-ENE6RG2P-DdOwICOg.js} +1 -1
  74. package/web/dist/assets/pilotswarm-CKh2MxfL.js +99 -0
  75. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CAwPVaYL.js → quadrantDiagram-ABIIQ3AL-B0Th1YO5.js} +1 -1
  76. package/web/dist/assets/{railroadDiagram-RFXS5EU6-Co102PkQ.js → railroadDiagram-RFXS5EU6-BF2XKMKf.js} +1 -1
  77. package/web/dist/assets/{requirementDiagram-TGXJPOKE-D19VlRSl.js → requirementDiagram-TGXJPOKE-Mr1iSwbS.js} +1 -1
  78. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Sd_a3ukL.js → sankeyDiagram-HTMAVEWB-CrOjVZyg.js} +1 -1
  79. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CojDRK1c.js → sequenceDiagram-DBY2YBRQ-H0X4Lydk.js} +1 -1
  80. package/web/dist/assets/{sizeCapture-X5ZJPWSS-XqXcxnDo.js → sizeCapture-X5ZJPWSS-Dus1QEqh.js} +1 -1
  81. package/web/dist/assets/{stateDiagram-2N3HPSRC-BcN_w699.js → stateDiagram-2N3HPSRC-BOBUSwdR.js} +1 -1
  82. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DABTDwFh.js +1 -0
  83. package/web/dist/assets/{swimlanes-5IMT3BWC-BcO6ai_I.js → swimlanes-5IMT3BWC-BoqT-Lh7.js} +2 -2
  84. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BF5mFN4t.js +8 -0
  85. package/web/dist/assets/{timeline-definition-FHXFAJF6-DOPfYxUq.js → timeline-definition-FHXFAJF6-BOrqUQHX.js} +1 -1
  86. package/web/dist/assets/{vennDiagram-L72KCM5P-BSKLkEOC.js → vennDiagram-L72KCM5P-DDkNL-Oo.js} +1 -1
  87. package/web/dist/assets/{wardleyDiagram-EHGQE667-BdcYAVw2.js → wardleyDiagram-EHGQE667-XUAJQ2X0.js} +1 -1
  88. package/web/dist/assets/{xychartDiagram-FW5EYKEG-C6EJT4jB.js → xychartDiagram-FW5EYKEG-ph69PV9y.js} +1 -1
  89. package/web/dist/index.html +3 -3
  90. package/ui/core/src/themes/dark-high-contrast.js +0 -56
  91. package/ui/core/src/themes/workspace-dark-rich.js +0 -65
  92. package/web/dist/assets/channel-BCzDctYg.js +0 -1
  93. package/web/dist/assets/classDiagram-OUVF2IWQ-ChqjVZil.js +0 -1
  94. package/web/dist/assets/classDiagram-v2-EOCWNBFH-ChqjVZil.js +0 -1
  95. package/web/dist/assets/index-D2B5bzSN.css +0 -1
  96. package/web/dist/assets/pilotswarm-DA0u7Cq_.js +0 -99
  97. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BV_w1D3C.js +0 -1
  98. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BnYdP0qP.js +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "PilotSwarm application package: terminal UI, browser portal + Web API server, and MCP server — one install, three bins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -81,7 +81,7 @@
81
81
  "ink": "^6.8.0",
82
82
  "jose": "^6.2.2",
83
83
  "mermaid": "^11.16.0",
84
- "pilotswarm-sdk": "0.5.32",
84
+ "pilotswarm-sdk": "0.5.34",
85
85
  "react": "^19.2.4",
86
86
  "react-dom": "^19.2.4",
87
87
  "ws": "^8.18.2"
package/tui/src/app.js CHANGED
@@ -464,6 +464,16 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
464
464
  controller.handleCommand(UI_COMMANDS.MODAL_PANE_NEXT).catch(() => {});
465
465
  return;
466
466
  }
467
+ // Agent-picker disclosure. The block this sits in ends in a
468
+ // `return`, so arrows never reach the pane handler below — the
469
+ // picker's "Enter or → to open" hint named a key the TUI dropped
470
+ // on the floor.
471
+ if (modal.type === "sessionAgentPicker" && (key.leftArrow || key.rightArrow)) {
472
+ controller.handleCommand(
473
+ key.rightArrow ? UI_COMMANDS.MODAL_PANE_NEXT : UI_COMMANDS.MODAL_PANE_PREV,
474
+ ).catch(() => {});
475
+ return;
476
+ }
467
477
  if (modal.type === "sessionOwnerFilter") {
468
478
  if (input === " ") {
469
479
  controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
@@ -378,6 +378,12 @@ function normalizeCreatableAgent(agent) {
378
378
  splashMobile: typeof agent?.splashMobile === "string" && agent.splashMobile.trim() ? agent.splashMobile : null,
379
379
  initialPrompt: typeof agent?.initialPrompt === "string" && agent.initialPrompt.trim() ? agent.initialPrompt : null,
380
380
  tools: Array.isArray(agent?.tools) ? agent.tools.filter(Boolean) : [],
381
+ // Composition, for the picker's per-package nesting. Baked agents
382
+ // rarely declare these, but a layered app's agents can — Waldemort's
383
+ // specialists are created by its router, and saying so puts them under
384
+ // it in the Built-in section instead of in a flat list of sixteen.
385
+ startedBy: Array.isArray(agent?.startedBy) ? agent.startedBy.filter(Boolean) : [],
386
+ supportsDirectStart: typeof agent?.supportsDirectStart === "boolean" ? agent.supportsDirectStart : undefined,
381
387
  };
382
388
  }
383
389
 
@@ -695,8 +701,13 @@ export class NodeSdkTransport {
695
701
  splash: typeof agent.splash === "string" && agent.splash.trim() ? agent.splash : undefined,
696
702
  splashMobile: typeof agent.splashMobile === "string" && agent.splashMobile.trim() ? agent.splashMobile : undefined,
697
703
  initialPrompt: typeof agent.initialPrompt === "string" && agent.initialPrompt.trim() ? agent.initialPrompt : undefined,
704
+ startedBy: Array.isArray(agent.startedBy) && agent.startedBy.length ? agent.startedBy : undefined,
705
+ supportsDirectStart: typeof agent.supportsDirectStart === "boolean" ? agent.supportsDirectStart : undefined,
698
706
  source: "package",
699
707
  packageName: pkg.name,
708
+ // Friendly section header. plugin.json "title" when the
709
+ // author set one; the DNS-label name otherwise.
710
+ packageTitle: pkg.active.manifest?.title || undefined,
700
711
  packageSemver: pkg.active.semver,
701
712
  scope: pkg.scope,
702
713
  mine,
@@ -713,6 +713,249 @@ function formatAgentDisplayTitle(agentName, title) {
713
713
  : "Agent";
714
714
  }
715
715
 
716
+ // ─── Agent picker: sections and composition ─────────────────────────────
717
+ //
718
+ // The picker used to be one flat list with `── Shared ──` / `── My agents ──`
719
+ // rules through it. That reads fine at six agents and not at all at forty:
720
+ // a deployment's own roster and four installed packages arrive as one
721
+ // undifferentiated column, and nothing says which agents belong together.
722
+ //
723
+ // So: two sections. BUILT-IN is every agent baked into the deployment (base
724
+ // PilotSwarm plus whatever the layered app ships), INSTALLED is one collapsed
725
+ // subsection per agent package. Inside a section, an agent that declares
726
+ // `startedBy` nests under the agent that starts it — a package's shape is
727
+ // visible before you start a session with it.
728
+
729
+ export const AGENT_PICKER_BUILTIN_KEY = "builtin";
730
+ // Installed packages split by ownership rather than sitting under one heading.
731
+ // "shared with the deployment" and "mine, private" are different trust stories
732
+ // — a shared package's agents run with the fleet's identity — and a user with
733
+ // only one kind should never be asked to read past a heading for the other.
734
+ export const AGENT_PICKER_SHARED_KEY = "installed:shared";
735
+ export const AGENT_PICKER_MINE_KEY = "installed:mine";
736
+
737
+ export function agentPickerPackageKey(agent) {
738
+ const scope = String(agent?.packageScope || agent?.scope || "shared");
739
+ const owner = String(agent?.ownerLabel || "");
740
+ return `pkg:${scope}:${owner}:${String(agent?.packageName || "")}`;
741
+ }
742
+
743
+ // Same normalization the agent RESOLVER uses (see normalizeAgentName in
744
+ // agent-package-format): the runtime matches names case- and
745
+ // punctuation-insensitively, so "Editor_In_Chief" and "editor-in-chief" are
746
+ // one agent. Matching on the raw string here would nest by a stricter rule
747
+ // than the thing it is describing.
748
+ function normalizeAgentKey(value) {
749
+ return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
750
+ }
751
+
752
+ /**
753
+ * Order a set of agents as a forest: entry points first, each immediately
754
+ * followed by the agents it creates.
755
+ *
756
+ * An entry point is an agent no OTHER agent in the same set creates. That is
757
+ * the whole definition — there is no `main` field to declare and keep honest,
758
+ * and a package with one, several or zero entry points all render correctly
759
+ * from the same rule.
760
+ *
761
+ * `startedBy` names that do not resolve inside the set are ignored rather than
762
+ * treated as errors: a package may legitimately be called by a deployment
763
+ * agent, and an agent must never vanish from the picker because of a typo.
764
+ */
765
+ function orderAgentsByComposition(agents) {
766
+ const byName = new Map();
767
+ for (const agent of agents) {
768
+ if (agent?.agentName) byName.set(normalizeAgentKey(agent.agentName), agent);
769
+ }
770
+
771
+ const childrenOf = new Map();
772
+ const parentOf = new Map();
773
+ for (const agent of agents) {
774
+ const self = normalizeAgentKey(agent.agentName);
775
+ const parents = (agent.startedBy || [])
776
+ .map((name) => normalizeAgentKey(name))
777
+ .filter((name) => name && byName.has(name) && name !== self);
778
+ if (parents.length === 0) continue;
779
+ // First resolvable creator wins the nesting slot. An agent started by
780
+ // two entry points is real; showing it twice would double-count the
781
+ // section and make Enter ambiguous.
782
+ const parent = parents[0];
783
+ parentOf.set(agent, parent);
784
+ if (!childrenOf.has(parent)) childrenOf.set(parent, []);
785
+ childrenOf.get(parent).push(agent);
786
+ }
787
+
788
+ // Whether a name in `startedBy` resolved to an agent in THIS set is the
789
+ // signal, not whether the field was present. A package that names a
790
+ // creator in another package — or misspells one — has an agent that
791
+ // nothing here starts, and it must stay an entry point rather than
792
+ // becoming permanently unstartable in a section that still claims it as
793
+ // one. (This is where the default for `supportsDirectStart` is applied;
794
+ // an explicit value always wins.)
795
+ const isNested = (agent) => parentOf.has(agent);
796
+ const startable = (agent) => (
797
+ typeof agent.supportsDirectStart === "boolean"
798
+ ? agent.supportsDirectStart
799
+ : !isNested(agent)
800
+ );
801
+
802
+ const ordered = [];
803
+ const emitted = new Set();
804
+ const emit = (agent, depth, parentName) => {
805
+ // Also the cycle guard: a → b → a stops here rather than recursing.
806
+ if (emitted.has(agent)) return;
807
+ emitted.add(agent);
808
+ ordered.push({
809
+ ...agent,
810
+ depth,
811
+ parentAgentName: parentName || null,
812
+ supportsDirectStart: startable(agent),
813
+ });
814
+ for (const child of childrenOf.get(normalizeAgentKey(agent.agentName)) || []) {
815
+ emit(child, depth + 1, agent.agentName);
816
+ }
817
+ };
818
+
819
+ for (const agent of agents) {
820
+ if (isNested(agent)) continue;
821
+ emit(agent, 0, null);
822
+ }
823
+ // Whatever is left is inside a cycle. Promote the CYCLE MEMBERS — the ones
824
+ // whose parent chain returns to themselves — and let the recursion carry
825
+ // their descendants down at the right depth. Promoting in array order
826
+ // instead would surface an ordinary sub-agent as an entry point purely
827
+ // because it happened to be listed before the cycle it hangs off.
828
+ const inCycle = (agent) => {
829
+ const seen = new Set();
830
+ let current = agent;
831
+ while (current && !seen.has(current)) {
832
+ seen.add(current);
833
+ current = byName.get(parentOf.get(current));
834
+ if (current === agent) return true;
835
+ }
836
+ return false;
837
+ };
838
+ for (const agent of agents) {
839
+ if (!emitted.has(agent) && inCycle(agent)) emit(agent, 0, null);
840
+ }
841
+ // Belt and braces: nothing may be dropped, whatever shape the graph is in.
842
+ for (const agent of agents) emit(agent, 0, null);
843
+ return ordered;
844
+ }
845
+
846
+ /**
847
+ * Flatten the catalog into the VISIBLE row list the picker navigates.
848
+ *
849
+ * Section headings are items, not decoration: arrow keys walk onto them and
850
+ * Enter toggles them, so the whole dialog is reachable without a mouse. Rows
851
+ * inside a collapsed section are simply absent, which is what keeps
852
+ * `selectedIndex` a plain index into this array.
853
+ */
854
+ /** Where the cursor starts. See the call site for why it is not just 0. */
855
+ export function agentPickerInitialIndex(items = []) {
856
+ const firstAgent = items.findIndex((item) => item.kind !== "section");
857
+ if (firstAgent >= 0) return firstAgent;
858
+ const firstClosed = items.findIndex((item) => item.kind === "section" && item.collapsed);
859
+ return firstClosed >= 0 ? firstClosed : 0;
860
+ }
861
+
862
+ export function buildAgentPickerItems(catalog = [], collapsedKeys = [], genericItem = null) {
863
+ const collapsed = new Set(collapsedKeys);
864
+ const items = [];
865
+
866
+ const builtins = catalog.filter((agent) => agent.builtin);
867
+ const packaged = catalog.filter((agent) => !agent.builtin);
868
+
869
+ // Generic sits ABOVE everything, outside both sections and outside every
870
+ // collapse. It is not an agent — it is the absence of one — and it is the
871
+ // most common pick, so it must never be a click away behind a twisty.
872
+ if (genericItem) items.push({ ...genericItem, depth: 0 });
873
+
874
+ if (builtins.length > 0) {
875
+ const builtinAgents = orderAgentsByComposition(builtins);
876
+ items.push({
877
+ id: `section:${AGENT_PICKER_BUILTIN_KEY}`,
878
+ kind: "section",
879
+ sectionKind: "builtin",
880
+ sectionKey: AGENT_PICKER_BUILTIN_KEY,
881
+ title: "Built-in",
882
+ meta: `${builtinAgents.length} agent${builtinAgents.length === 1 ? "" : "s"}`,
883
+ collapsed: collapsed.has(AGENT_PICKER_BUILTIN_KEY),
884
+ depth: 0,
885
+ });
886
+ if (!collapsed.has(AGENT_PICKER_BUILTIN_KEY)) {
887
+ // One level in from the header, so the section reads as a
888
+ // container rather than as a label floating above a flat list.
889
+ items.push(...builtinAgents.map((agent) => ({ ...agent, depth: agent.depth + 1 })));
890
+ }
891
+ }
892
+
893
+ // An empty category is omitted entirely rather than shown reading "0
894
+ // packages" — a heading whose only content is its own emptiness is noise.
895
+ for (const [sectionKey, title, belongs] of [
896
+ [AGENT_PICKER_SHARED_KEY, "Installed · Shared", (agent) => agent.group !== "mine"],
897
+ [AGENT_PICKER_MINE_KEY, "Installed · Yours", (agent) => agent.group === "mine"],
898
+ ]) {
899
+ const members = packaged.filter(belongs);
900
+ if (members.length === 0) continue;
901
+
902
+ const groups = new Map();
903
+ for (const agent of members) {
904
+ const key = agentPickerPackageKey(agent);
905
+ if (!groups.has(key)) groups.set(key, []);
906
+ groups.get(key).push(agent);
907
+ }
908
+ items.push({
909
+ id: `section:${sectionKey}`,
910
+ kind: "section",
911
+ sectionKind: "installed",
912
+ sectionKey,
913
+ title,
914
+ meta: `${groups.size} package${groups.size === 1 ? "" : "s"}`,
915
+ collapsed: collapsed.has(sectionKey),
916
+ depth: 0,
917
+ });
918
+ if (collapsed.has(sectionKey)) continue;
919
+
920
+ const keys = [...groups.keys()].sort((a, b) => {
921
+ const at = groups.get(a)[0];
922
+ const bt = groups.get(b)[0];
923
+ return String(at.packageTitle || at.packageName || "")
924
+ .localeCompare(String(bt.packageTitle || bt.packageName || ""));
925
+ });
926
+ for (const key of keys) {
927
+ const packageAgents = orderAgentsByComposition(groups.get(key));
928
+ const first = packageAgents[0] || {};
929
+ const entries = packageAgents.filter((agent) => agent.depth === 0).length;
930
+ items.push({
931
+ id: `section:${key}`,
932
+ kind: "section",
933
+ sectionKind: "package",
934
+ sectionKey: key,
935
+ title: first.packageTitle || first.packageName || "Package",
936
+ packageName: first.packageName || null,
937
+ packageSemver: first.packageSemver || null,
938
+ packageScope: first.packageScope || null,
939
+ ownerLabel: first.ownerLabel || null,
940
+ mine: first.group === "mine",
941
+ // No scope badge: the section it sits in already says which
942
+ // shelf this is, and repeating "[shared]" on every row under
943
+ // "Installed · Shared" is the kind of noise that makes a list
944
+ // harder to scan, not easier.
945
+ meta: `${entries} ${entries === 1 ? "entry" : "entries"} · ${packageAgents.length} agent${packageAgents.length === 1 ? "" : "s"}`,
946
+ collapsed: collapsed.has(key),
947
+ depth: 1,
948
+ });
949
+ if (collapsed.has(key)) continue;
950
+ // Package rows sit one level in from their section header, so
951
+ // their sub-agents start at depth 2.
952
+ items.push(...packageAgents.map((agent) => ({ ...agent, depth: agent.depth + 2 })));
953
+ }
954
+ }
955
+
956
+ return items;
957
+ }
958
+
716
959
  function buildPromptAttachmentToken(filename) {
717
960
  return `📎 ${String(filename || "").trim()}`;
718
961
  }
@@ -4417,9 +4660,14 @@ export class PilotSwarmUiController {
4417
4660
  const activeSession = state.sessions?.activeSessionId
4418
4661
  ? state.sessions.byId[state.sessions.activeSessionId]
4419
4662
  : null;
4420
- const groupId = activeSession?.isGroup
4421
- ? activeSession.groupId || sessionGroupIdFromRowId(activeSession.sessionId)
4422
- : activeSession?.groupId;
4663
+ // ONLY when the selected row is the folder itself. Selecting a folder
4664
+ // and pressing New is the user pointing at it; inheriting the group
4665
+ // from a selected MEMBER is not — it filed new sessions inside
4666
+ // whichever folder happened to hold the last session read, so a new
4667
+ // session appeared buried mid-list instead of at the end where the
4668
+ // sort puts an unplaced row.
4669
+ if (!activeSession?.isGroup) return options;
4670
+ const groupId = activeSession.groupId || sessionGroupIdFromRowId(activeSession.sessionId);
4423
4671
  return groupId ? { ...options, groupId } : options;
4424
4672
  }
4425
4673
 
@@ -4503,9 +4751,9 @@ export class PilotSwarmUiController {
4503
4751
  return;
4504
4752
  }
4505
4753
 
4506
- // Display order IS item order (Shared My agents → Generic) so
4507
- // keyboard navigation walks the list visually; the selector only
4508
- // inserts non-clickable group headings between the segments.
4754
+ // The CATALOG (every agent, section-agnostic). The visible row list is
4755
+ // derived from it by buildAgentPickerItems, which is re-run whenever a
4756
+ // section is expanded or collapsed.
4509
4757
  const agentItems = [];
4510
4758
  for (const agent of agents) {
4511
4759
  const agentName = String(agent?.name || "").trim();
@@ -4533,17 +4781,24 @@ export class PilotSwarmUiController {
4533
4781
  ? "mine"
4534
4782
  : "shared",
4535
4783
  packageName: agent?.packageName || null,
4784
+ packageTitle: agent?.packageTitle || null,
4536
4785
  packageSemver: agent?.packageSemver || null,
4537
4786
  packageScope: agent?.scope || null,
4787
+ ownerLabel: agent?.ownerLabel || null,
4788
+ startedBy: Array.isArray(agent?.startedBy) ? agent.startedBy.filter(Boolean) : [],
4789
+ // RAW: undefined means "not declared". The default is applied
4790
+ // in orderAgentsByComposition, which is the only place that
4791
+ // knows whether a creator actually resolved.
4792
+ supportsDirectStart: typeof agent?.supportsDirectStart === "boolean" ? agent.supportsDirectStart : undefined,
4538
4793
  builtin: agent?.source !== "package",
4539
4794
  });
4540
4795
  }
4541
- const items = [
4796
+ const catalog = [
4542
4797
  ...agentItems.filter((item) => item.group === "shared"),
4543
4798
  ...agentItems.filter((item) => item.group === "mine"),
4544
4799
  ];
4545
- if (allowGeneric) {
4546
- items.push({
4800
+ const genericItem = allowGeneric
4801
+ ? {
4547
4802
  id: "__generic__",
4548
4803
  kind: "generic",
4549
4804
  group: "generic",
@@ -4553,16 +4808,39 @@ export class PilotSwarmUiController {
4553
4808
  splash: null,
4554
4809
  splashMobile: null,
4555
4810
  initialPrompt: null,
4556
- });
4557
- }
4811
+ supportsDirectStart: true,
4812
+ }
4813
+ : null;
4814
+
4815
+ // EVERYTHING starts closed. The dialog opens as a short menu of
4816
+ // categories — Generic, then whichever of Built-in / Shared / Yours
4817
+ // this deployment actually has — and you open the one you want. A
4818
+ // section expanded by default is a section every other user has to
4819
+ // scroll past.
4820
+ const collapsed = [
4821
+ AGENT_PICKER_BUILTIN_KEY,
4822
+ AGENT_PICKER_SHARED_KEY,
4823
+ AGENT_PICKER_MINE_KEY,
4824
+ ...new Set(catalog.filter((item) => !item.builtin).map((item) => agentPickerPackageKey(item))),
4825
+ ];
4826
+ const items = buildAgentPickerItems(catalog, collapsed, genericItem);
4558
4827
 
4559
4828
  this.dispatch({
4560
4829
  type: "ui/modal",
4561
4830
  modal: {
4562
4831
  type: "sessionAgentPicker",
4563
4832
  title: "Select agent for new session",
4833
+ catalog,
4834
+ genericItem,
4835
+ collapsed,
4564
4836
  items,
4565
- selectedIndex: 0,
4837
+ // Land somewhere Enter does something useful. First choice is a
4838
+ // real agent row; with none visible (no generic, no baked
4839
+ // agents, every package closed) fall back to the first CLOSED
4840
+ // package, whose Enter opens it. Landing on an open section
4841
+ // header — which findIndex's -1 used to do via Math.max — made
4842
+ // the very first keystroke collapse the whole list.
4843
+ selectedIndex: agentPickerInitialIndex(items),
4566
4844
  previousFocus,
4567
4845
  sessionOptions: options,
4568
4846
  },
@@ -4838,10 +5116,16 @@ export class PilotSwarmUiController {
4838
5116
  }
4839
5117
 
4840
5118
  openThemePicker() {
5119
+ // listThemes() is already ordered by group then label, so the picker
5120
+ // renders a heading wherever `group` changes rather than re-sorting.
5121
+ // The list itself stays FLAT: headings are drawn between rows, never
5122
+ // as rows, so selectedIndex keeps addressing themes and arrow-key
5123
+ // navigation never lands on a heading.
4841
5124
  const themes = listThemes().map((theme) => ({
4842
5125
  id: theme.id,
4843
5126
  label: theme.label,
4844
5127
  description: theme.description,
5128
+ group: theme.group,
4845
5129
  page: theme.page,
4846
5130
  terminal: theme.terminal,
4847
5131
  tui: theme.tui,
@@ -5839,6 +6123,72 @@ export class PilotSwarmUiController {
5839
6123
  this.dispatch({ type: "ui/status", text: "Connected" });
5840
6124
  }
5841
6125
 
6126
+ /**
6127
+ * Open or close one agent-picker section.
6128
+ *
6129
+ * The visible row list is rebuilt from the catalog, and selection is
6130
+ * carried by ITEM ID rather than index: collapsing a section above the
6131
+ * cursor shifts every index below it, and re-using the old number would
6132
+ * silently move the highlight onto a different agent.
6133
+ */
6134
+ toggleAgentPickerSection(sectionKey, force = null) {
6135
+ const modal = this.getState().ui.modal;
6136
+ if (!modal || modal.type !== "sessionAgentPicker" || !sectionKey) return;
6137
+ const collapsed = new Set(modal.collapsed || []);
6138
+ const shouldCollapse = force === null ? !collapsed.has(sectionKey) : force;
6139
+ if (shouldCollapse === collapsed.has(sectionKey)) return;
6140
+ if (shouldCollapse) collapsed.add(sectionKey);
6141
+ else collapsed.delete(sectionKey);
6142
+
6143
+ const nextCollapsed = [...collapsed];
6144
+ const items = buildAgentPickerItems(modal.catalog || [], nextCollapsed, modal.genericItem || null);
6145
+ const selectedId = modal.items?.[modal.selectedIndex || 0]?.id;
6146
+ let nextIndex = items.findIndex((item) => item.id === selectedId);
6147
+ if (nextIndex < 0) {
6148
+ // The selected row was inside the section that just closed — land
6149
+ // on that section's own header, which is where it went.
6150
+ nextIndex = Math.max(0, items.findIndex((item) => item.sectionKey === sectionKey));
6151
+ }
6152
+ this.dispatch({
6153
+ type: "ui/modal",
6154
+ modal: { ...modal, collapsed: nextCollapsed, items, selectedIndex: nextIndex },
6155
+ });
6156
+ }
6157
+
6158
+ /**
6159
+ * Left/right on the agent picker. Right opens a closed section (or steps
6160
+ * into it); left closes an open one, or jumps to the parent header when
6161
+ * the cursor is already on a leaf — the same shape as the session tree.
6162
+ */
6163
+ moveAgentPickerSection(delta) {
6164
+ const modal = this.getState().ui.modal;
6165
+ if (!modal || modal.type !== "sessionAgentPicker") return false;
6166
+ const items = Array.isArray(modal.items) ? modal.items : [];
6167
+ const index = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, items.length - 1));
6168
+ const item = items[index];
6169
+ if (!item) return false;
6170
+
6171
+ if (delta > 0) {
6172
+ if (item.kind === "section" && item.collapsed) {
6173
+ this.toggleAgentPickerSection(item.sectionKey, false);
6174
+ return true;
6175
+ }
6176
+ return false;
6177
+ }
6178
+
6179
+ if (item.kind === "section" && !item.collapsed) {
6180
+ this.toggleAgentPickerSection(item.sectionKey, true);
6181
+ return true;
6182
+ }
6183
+ for (let i = index - 1; i >= 0; i -= 1) {
6184
+ if (items[i].kind === "section" && items[i].depth < item.depth) {
6185
+ this.dispatch({ type: "ui/modalSelection", index: i });
6186
+ return true;
6187
+ }
6188
+ }
6189
+ return false;
6190
+ }
6191
+
5842
6192
  moveModalSelection(delta) {
5843
6193
  const modal = this.getState().ui.modal;
5844
6194
  if (!modal) return;
@@ -5883,6 +6233,10 @@ export class PilotSwarmUiController {
5883
6233
 
5884
6234
  moveModalPane(delta) {
5885
6235
  const modal = this.getState().ui.modal;
6236
+ if (modal?.type === "sessionAgentPicker") {
6237
+ this.moveAgentPickerSection(delta);
6238
+ return;
6239
+ }
5886
6240
  if (!modal || (modal.type !== "logFilter" && modal.type !== "filesFilter" && modal.type !== "historyFormat") || !Array.isArray(modal.items) || modal.items.length === 0) return;
5887
6241
  const current = Math.max(0, Number(modal.selectedIndex) || 0);
5888
6242
  const next = (current + delta + modal.items.length) % modal.items.length;
@@ -6052,6 +6406,21 @@ export class PilotSwarmUiController {
6052
6406
  }
6053
6407
  if (modal.type === "sessionAgentPicker") {
6054
6408
  const item = modal.items?.[modal.selectedIndex || 0];
6409
+ // Enter on a heading opens or closes it; the dialog stays up.
6410
+ if (item?.kind === "section") {
6411
+ this.toggleAgentPickerSection(item.sectionKey);
6412
+ return;
6413
+ }
6414
+ // A called-only agent is rendered so the package's composition is
6415
+ // visible, but starting one cold is not a thing you can do. Refuse
6416
+ // here rather than letting the create fail after the dialog closes.
6417
+ if (item && item.kind !== "generic" && item.supportsDirectStart === false) {
6418
+ this.dispatch({
6419
+ type: "ui/status",
6420
+ text: `${item.title || item.agentName} is created by another agent and cannot be started on its own`,
6421
+ });
6422
+ return;
6423
+ }
6055
6424
  const previousFocus = modal.previousFocus;
6056
6425
  const sessionOptions = modal.sessionOptions || {};
6057
6426
  this.dispatch({ type: "ui/modal", modal: null });