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.
- package/package.json +2 -2
- package/tui/src/app.js +10 -0
- package/tui/src/node-sdk-transport.js +11 -0
- package/ui/core/src/controller.js +381 -12
- package/ui/core/src/reducer.js +125 -28
- package/ui/core/src/selectors.js +158 -268
- package/ui/core/src/state.js +6 -1
- package/ui/core/src/themes/catppuccin-mocha.js +2 -1
- package/ui/core/src/themes/cobalt2.js +2 -1
- package/ui/core/src/themes/doom.js +2 -1
- package/ui/core/src/themes/dracula.js +2 -1
- package/ui/core/src/themes/github-dark.js +2 -1
- package/ui/core/src/themes/github-light.js +2 -1
- package/ui/core/src/themes/gruvbox-dark.js +2 -1
- package/ui/core/src/themes/hacker-x-matrix.js +2 -1
- package/ui/core/src/themes/hacker-x-orion-prime.js +2 -1
- package/ui/core/src/themes/helpers.js +22 -4
- package/ui/core/src/themes/high-contrast-mono.js +2 -1
- package/ui/core/src/themes/index.js +24 -5
- package/ui/core/src/themes/ms-dos.js +2 -1
- package/ui/core/src/themes/noctis-obscuro.js +2 -1
- package/ui/core/src/themes/noctis.js +2 -1
- package/ui/core/src/themes/rust.js +125 -0
- package/ui/core/src/themes/spongebob.js +110 -0
- package/ui/core/src/themes/terminal-green.js +2 -1
- package/ui/core/src/themes/win95.js +2 -1
- package/ui/core/src/themes/winamp.js +2 -1
- package/ui/core/src/themes/workspace-dark.js +2 -1
- package/ui/react/src/web-app.js +240 -266
- package/web/dist/assets/{abnfDiagram-VRR7QNED-mXFQ4a3c.js → abnfDiagram-VRR7QNED-BFOimVWK.js} +1 -1
- package/web/dist/assets/{arc-BPuuvbge.js → arc-uUHjvwEo.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Dt4uUYIe.js → architectureDiagram-ZJ3FMSHR-CxeX_vea.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Ax8Q22vV.js → blockDiagram-677ZJIJ3-Cno8kltG.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CPiukyHt.js → c4Diagram-LMCZKHZV-viONVybf.js} +1 -1
- package/web/dist/assets/channel-IURmSsjn.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-VJLqCZJX.js → chunk-2Q5K7J3B-CKfICIGD.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-DqOshxhf.js → chunk-32BRIVSS-CxmwEY56.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-KPqV21jV.js → chunk-5VM5RSS4-_eVYLGll.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DfhiCVl_.js → chunk-EX3LRPZG-C4aqbjrf.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-H6khJPuW.js → chunk-JWPE2WC7-Bh8ZxrI8.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BKMAzQxj.js → chunk-MOJQB5TN-BXgLlWVP.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DNXRTxtj.js → chunk-RYQCIY6F-B1rMIdfF.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-B56PM6Gv.js → chunk-V7JOEXUC-C3AnSHXk.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-Boi3hGkB.js → chunk-VR4S4FIN-CeoMJIBs.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DmJyn890.js → chunk-XXDRQBXY-DzaxQ9iP.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-FsfpM3iu.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-FsfpM3iu.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BRdFlFWt.js → cose-bilkent-JH36ORCC-G46fvUOx.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DqE-xib2.js → cynefin-VYW2F7L2-DXwqVhWA.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-D-QBZjIL.js → cynefinDiagram-TSTJHNR4-DNNFmaXb.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DYu9oQV6.js → dagre-VKFMJZFB-BCfh-h3o.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DOwYccCg.js → diagram-FQU43EPY-CyG9b6bj.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DTSEmrPr.js → diagram-G47NLZAW-DdxDahK3.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-OqbzzquA.js → diagram-NH7WQ7WH-CjrZ_eja.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BMVG6A8I.js → diagram-OA4YK3LP-5NAjQpk2.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Rn2Q5xaz.js → diagram-WEI45ONY-fLuDb54-.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-aL32_v5y.js → ebnfDiagram-CCIWWBDH-DL9qnZpa.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DKCpyczx.js → erDiagram-Q63AITRT-C_ynfrWY.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CrZTPgR_.js → flowDiagram-23GEKE2U-dYuH8HYv.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-DnSE1OtC.js → ganttDiagram-NO4QXBWP-e8Meh3f3.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C5SeYPUR.js → gitGraphDiagram-IHSO6WYX-D_tZ4VL9.js} +1 -1
- package/web/dist/assets/index-Ct7SQv1M.css +1 -0
- package/web/dist/assets/{index-B-TEKrRB.js → index-DcQes9h8.js} +6 -6
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DDnJZp0X.js → infoDiagram-FWYZ7A6U-Dffhqp73.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-ewhzfFkq.js → ishikawaDiagram-FXEZZL3T-DYt9H7kU.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-T0iWWkLM.js → journeyDiagram-5HDEW3XC-Qv1vVu0F.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-D1MFQZjb.js → kanban-definition-HUTT4EX6-CTAUo40G.js} +1 -1
- package/web/dist/assets/{linear-BKfPK9wE.js → linear-CqLsWFLj.js} +1 -1
- package/web/dist/assets/{mermaid.core-CyzD7dTc.js → mermaid.core-okRCGPQQ.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-CWXtcTlP.js → mindmap-definition-LN4V7U3C-DVOOrIwD.js} +1 -1
- package/web/dist/assets/{papaparse.min-DLUoV5k5.js → papaparse.min-Ddflu8oC.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BFjZun-r.js → pegDiagram-2B236MQR-BLJlC0MZ.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-DMDtW4g7.js → pieDiagram-ENE6RG2P-DdOwICOg.js} +1 -1
- package/web/dist/assets/pilotswarm-CKh2MxfL.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CAwPVaYL.js → quadrantDiagram-ABIIQ3AL-B0Th1YO5.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-Co102PkQ.js → railroadDiagram-RFXS5EU6-BF2XKMKf.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-D19VlRSl.js → requirementDiagram-TGXJPOKE-Mr1iSwbS.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Sd_a3ukL.js → sankeyDiagram-HTMAVEWB-CrOjVZyg.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CojDRK1c.js → sequenceDiagram-DBY2YBRQ-H0X4Lydk.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-XqXcxnDo.js → sizeCapture-X5ZJPWSS-Dus1QEqh.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BcN_w699.js → stateDiagram-2N3HPSRC-BOBUSwdR.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DABTDwFh.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BcO6ai_I.js → swimlanes-5IMT3BWC-BoqT-Lh7.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BF5mFN4t.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DOPfYxUq.js → timeline-definition-FHXFAJF6-BOrqUQHX.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BSKLkEOC.js → vennDiagram-L72KCM5P-DDkNL-Oo.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BdcYAVw2.js → wardleyDiagram-EHGQE667-XUAJQ2X0.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-C6EJT4jB.js → xychartDiagram-FW5EYKEG-ph69PV9y.js} +1 -1
- package/web/dist/index.html +3 -3
- package/ui/core/src/themes/dark-high-contrast.js +0 -56
- package/ui/core/src/themes/workspace-dark-rich.js +0 -65
- package/web/dist/assets/channel-BCzDctYg.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-ChqjVZil.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-ChqjVZil.js +0 -1
- package/web/dist/assets/index-D2B5bzSN.css +0 -1
- package/web/dist/assets/pilotswarm-DA0u7Cq_.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BV_w1D3C.js +0 -1
- 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.
|
|
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.
|
|
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
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
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
|
-
//
|
|
4507
|
-
//
|
|
4508
|
-
//
|
|
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
|
|
4796
|
+
const catalog = [
|
|
4542
4797
|
...agentItems.filter((item) => item.group === "shared"),
|
|
4543
4798
|
...agentItems.filter((item) => item.group === "mine"),
|
|
4544
4799
|
];
|
|
4545
|
-
|
|
4546
|
-
|
|
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
|
-
|
|
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 });
|