pilotswarm 0.5.33 → 0.5.35
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 +533 -24
- package/ui/core/src/formatting.js +13 -1
- package/ui/core/src/history.js +8 -0
- package/ui/core/src/reducer.js +172 -28
- package/ui/core/src/selectors.js +160 -272
- package/ui/core/src/state.js +34 -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/duroxide.js +136 -0
- 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 +26 -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 +1153 -307
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DlP1x0QR.js → abnfDiagram-VRR7QNED-BwJEohLG.js} +1 -1
- package/web/dist/assets/{arc-Zo_8acjl.js → arc-CWUJ_S5s.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bc18aajT.js → architectureDiagram-ZJ3FMSHR-i6mrLxuE.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-DU76JwlX.js → blockDiagram-677ZJIJ3-C_KG-qPQ.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-COqs9Vhg.js → c4Diagram-LMCZKHZV-BBJH7hbB.js} +1 -1
- package/web/dist/assets/channel-5Ddqi9ib.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-DLg4HU8R.js → chunk-2Q5K7J3B-CXwaFcaM.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-CU0Q4Vsz.js → chunk-32BRIVSS-C8ujjkK-.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BqRSeab5.js → chunk-5VM5RSS4-M9rz7YYk.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DenHmuGS.js → chunk-EX3LRPZG-bk6tRxOA.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BUpE28jH.js → chunk-JWPE2WC7-DZ4WSHGH.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-D4WdKP4p.js → chunk-MOJQB5TN-B5-y6tMF.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DJgnfeyv.js → chunk-RYQCIY6F-yKcnHxm0.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DII2R5xT.js → chunk-V7JOEXUC-CpoNcATD.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-BJ_02Sft.js → chunk-VR4S4FIN-Bf1yD2fn.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DTi_vPws.js → chunk-XXDRQBXY-EnPvDVhP.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B8u1fZiB.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B8u1fZiB.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-DzdVN7Sx.js → cose-bilkent-JH36ORCC-qhKoA2qj.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CLw-4-yw.js → cynefin-VYW2F7L2-jcyYelJS.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-P8h9O8wh.js → cynefinDiagram-TSTJHNR4-D4RLufLP.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-CDArowlh.js → dagre-VKFMJZFB-C9BA3GTO.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-GiMWEGCC.js → diagram-FQU43EPY-DbSSrVUJ.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CpiRJOW2.js → diagram-G47NLZAW-3egISQXQ.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-BOdTWSov.js → diagram-NH7WQ7WH-8IKEMGRg.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BPKB7KD5.js → diagram-OA4YK3LP-VnaogqtK.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-CPS66m-d.js → diagram-WEI45ONY-DxigXH0T.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-nbpYqaxu.js → ebnfDiagram-CCIWWBDH-BSkNwJ9x.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-BF547i7I.js → erDiagram-Q63AITRT-CKN8JXl6.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CWIuM8_R.js → flowDiagram-23GEKE2U-DDaD5ExP.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-DZRmOOgZ.js → ganttDiagram-NO4QXBWP-tOrBOuoI.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-DQASL6yJ.js → gitGraphDiagram-IHSO6WYX-Cdp867ON.js} +1 -1
- package/web/dist/assets/{index-MikpTOty.js → index-BmwU43_u.js} +6 -6
- package/web/dist/assets/index-D-nA7gUe.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DlK6reuU.js → infoDiagram-FWYZ7A6U-DwcKHD2h.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-DOiJ-xI_.js → ishikawaDiagram-FXEZZL3T-CWffK5kz.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B4EVeK0i.js → journeyDiagram-5HDEW3XC-BEAgS-EZ.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-eBT_l0PV.js → kanban-definition-HUTT4EX6-xDm-mRRj.js} +1 -1
- package/web/dist/assets/{linear-CGPMpgYy.js → linear-oP9MA0as.js} +1 -1
- package/web/dist/assets/{mermaid.core-CMg-D4qI.js → mermaid.core-DgXXt1OS.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-COIkL1R_.js → mindmap-definition-LN4V7U3C-BBQgVrYC.js} +1 -1
- package/web/dist/assets/{papaparse.min-BkzE5bQr.js → papaparse.min-CR6JmBWm.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-nXLPVk3y.js → pegDiagram-2B236MQR-C2O0cf3j.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-CLFUyga1.js → pieDiagram-ENE6RG2P-D08pUtYU.js} +1 -1
- package/web/dist/assets/pilotswarm-D7NLCbuc.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BHSt68p5.js → quadrantDiagram-ABIIQ3AL-D-Z7YoEK.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-CyohRm8J.js → railroadDiagram-RFXS5EU6-ClkdveLI.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-CQTikEcB.js → requirementDiagram-TGXJPOKE-IY4xbn-Q.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CxDlhil0.js → sankeyDiagram-HTMAVEWB-Cpt5i7KM.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-n4ZRJPY0.js → sequenceDiagram-DBY2YBRQ-c07lALcz.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-J_LGaCmN.js → sizeCapture-X5ZJPWSS-CmCDiNbI.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-CWavHbpD.js → stateDiagram-2N3HPSRC-Dueu17zX.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-AGpIV7wW.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-l-bxBeeJ.js → swimlanes-5IMT3BWC-Csc2Fup8.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTGZSV0N.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-ChA5iURW.js → timeline-definition-FHXFAJF6-4-lf702e.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-DVkk3iKL.js → vennDiagram-L72KCM5P-CmlaKpLI.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CrN8c7Sv.js → wardleyDiagram-EHGQE667-B9Qe44pi.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Hym-Hdkg.js → xychartDiagram-FW5EYKEG-BFcc0CLn.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-DA38xcdK.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-1k2XCPxl.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-1k2XCPxl.js +0 -1
- package/web/dist/assets/index-Do0UrHeI.css +0 -1
- package/web/dist/assets/pilotswarm-C17s5NOb.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-ClqTpkzd.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Bfar1rKu.js +0 -8
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
MIN_ACTIVITY_PANE_HEIGHT,
|
|
25
25
|
MIN_INSPECTOR_PANE_HEIGHT,
|
|
26
26
|
DEFAULT_ACTIVITY_PANE_RATIO,
|
|
27
|
+
DEFAULT_LEFT_PANE_RATIO,
|
|
28
|
+
COLLAPSE_RIGHT_THRESHOLD,
|
|
27
29
|
normalizeFocusRegion,
|
|
28
30
|
} from "./layout.js";
|
|
29
31
|
import { parseTerminalMarkupRuns } from "./formatting.js";
|
|
@@ -680,7 +682,7 @@ function getRenameSessionPrefix(session) {
|
|
|
680
682
|
if (separatorIndex > 0) {
|
|
681
683
|
return currentTitle.slice(0, separatorIndex).trim() || null;
|
|
682
684
|
}
|
|
683
|
-
return
|
|
685
|
+
return null;
|
|
684
686
|
}
|
|
685
687
|
|
|
686
688
|
function getRenameSessionMaxLength(session) {
|
|
@@ -713,6 +715,249 @@ function formatAgentDisplayTitle(agentName, title) {
|
|
|
713
715
|
: "Agent";
|
|
714
716
|
}
|
|
715
717
|
|
|
718
|
+
// ─── Agent picker: sections and composition ─────────────────────────────
|
|
719
|
+
//
|
|
720
|
+
// The picker used to be one flat list with `── Shared ──` / `── My agents ──`
|
|
721
|
+
// rules through it. That reads fine at six agents and not at all at forty:
|
|
722
|
+
// a deployment's own roster and four installed packages arrive as one
|
|
723
|
+
// undifferentiated column, and nothing says which agents belong together.
|
|
724
|
+
//
|
|
725
|
+
// So: two sections. BUILT-IN is every agent baked into the deployment (base
|
|
726
|
+
// PilotSwarm plus whatever the layered app ships), INSTALLED is one collapsed
|
|
727
|
+
// subsection per agent package. Inside a section, an agent that declares
|
|
728
|
+
// `startedBy` nests under the agent that starts it — a package's shape is
|
|
729
|
+
// visible before you start a session with it.
|
|
730
|
+
|
|
731
|
+
export const AGENT_PICKER_BUILTIN_KEY = "builtin";
|
|
732
|
+
// Installed packages split by ownership rather than sitting under one heading.
|
|
733
|
+
// "shared with the deployment" and "mine, private" are different trust stories
|
|
734
|
+
// — a shared package's agents run with the fleet's identity — and a user with
|
|
735
|
+
// only one kind should never be asked to read past a heading for the other.
|
|
736
|
+
export const AGENT_PICKER_SHARED_KEY = "installed:shared";
|
|
737
|
+
export const AGENT_PICKER_MINE_KEY = "installed:mine";
|
|
738
|
+
|
|
739
|
+
export function agentPickerPackageKey(agent) {
|
|
740
|
+
const scope = String(agent?.packageScope || agent?.scope || "shared");
|
|
741
|
+
const owner = String(agent?.ownerLabel || "");
|
|
742
|
+
return `pkg:${scope}:${owner}:${String(agent?.packageName || "")}`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Same normalization the agent RESOLVER uses (see normalizeAgentName in
|
|
746
|
+
// agent-package-format): the runtime matches names case- and
|
|
747
|
+
// punctuation-insensitively, so "Editor_In_Chief" and "editor-in-chief" are
|
|
748
|
+
// one agent. Matching on the raw string here would nest by a stricter rule
|
|
749
|
+
// than the thing it is describing.
|
|
750
|
+
function normalizeAgentKey(value) {
|
|
751
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Order a set of agents as a forest: entry points first, each immediately
|
|
756
|
+
* followed by the agents it creates.
|
|
757
|
+
*
|
|
758
|
+
* An entry point is an agent no OTHER agent in the same set creates. That is
|
|
759
|
+
* the whole definition — there is no `main` field to declare and keep honest,
|
|
760
|
+
* and a package with one, several or zero entry points all render correctly
|
|
761
|
+
* from the same rule.
|
|
762
|
+
*
|
|
763
|
+
* `startedBy` names that do not resolve inside the set are ignored rather than
|
|
764
|
+
* treated as errors: a package may legitimately be called by a deployment
|
|
765
|
+
* agent, and an agent must never vanish from the picker because of a typo.
|
|
766
|
+
*/
|
|
767
|
+
function orderAgentsByComposition(agents) {
|
|
768
|
+
const byName = new Map();
|
|
769
|
+
for (const agent of agents) {
|
|
770
|
+
if (agent?.agentName) byName.set(normalizeAgentKey(agent.agentName), agent);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const childrenOf = new Map();
|
|
774
|
+
const parentOf = new Map();
|
|
775
|
+
for (const agent of agents) {
|
|
776
|
+
const self = normalizeAgentKey(agent.agentName);
|
|
777
|
+
const parents = (agent.startedBy || [])
|
|
778
|
+
.map((name) => normalizeAgentKey(name))
|
|
779
|
+
.filter((name) => name && byName.has(name) && name !== self);
|
|
780
|
+
if (parents.length === 0) continue;
|
|
781
|
+
// First resolvable creator wins the nesting slot. An agent started by
|
|
782
|
+
// two entry points is real; showing it twice would double-count the
|
|
783
|
+
// section and make Enter ambiguous.
|
|
784
|
+
const parent = parents[0];
|
|
785
|
+
parentOf.set(agent, parent);
|
|
786
|
+
if (!childrenOf.has(parent)) childrenOf.set(parent, []);
|
|
787
|
+
childrenOf.get(parent).push(agent);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Whether a name in `startedBy` resolved to an agent in THIS set is the
|
|
791
|
+
// signal, not whether the field was present. A package that names a
|
|
792
|
+
// creator in another package — or misspells one — has an agent that
|
|
793
|
+
// nothing here starts, and it must stay an entry point rather than
|
|
794
|
+
// becoming permanently unstartable in a section that still claims it as
|
|
795
|
+
// one. (This is where the default for `supportsDirectStart` is applied;
|
|
796
|
+
// an explicit value always wins.)
|
|
797
|
+
const isNested = (agent) => parentOf.has(agent);
|
|
798
|
+
const startable = (agent) => (
|
|
799
|
+
typeof agent.supportsDirectStart === "boolean"
|
|
800
|
+
? agent.supportsDirectStart
|
|
801
|
+
: !isNested(agent)
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
const ordered = [];
|
|
805
|
+
const emitted = new Set();
|
|
806
|
+
const emit = (agent, depth, parentName) => {
|
|
807
|
+
// Also the cycle guard: a → b → a stops here rather than recursing.
|
|
808
|
+
if (emitted.has(agent)) return;
|
|
809
|
+
emitted.add(agent);
|
|
810
|
+
ordered.push({
|
|
811
|
+
...agent,
|
|
812
|
+
depth,
|
|
813
|
+
parentAgentName: parentName || null,
|
|
814
|
+
supportsDirectStart: startable(agent),
|
|
815
|
+
});
|
|
816
|
+
for (const child of childrenOf.get(normalizeAgentKey(agent.agentName)) || []) {
|
|
817
|
+
emit(child, depth + 1, agent.agentName);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
for (const agent of agents) {
|
|
822
|
+
if (isNested(agent)) continue;
|
|
823
|
+
emit(agent, 0, null);
|
|
824
|
+
}
|
|
825
|
+
// Whatever is left is inside a cycle. Promote the CYCLE MEMBERS — the ones
|
|
826
|
+
// whose parent chain returns to themselves — and let the recursion carry
|
|
827
|
+
// their descendants down at the right depth. Promoting in array order
|
|
828
|
+
// instead would surface an ordinary sub-agent as an entry point purely
|
|
829
|
+
// because it happened to be listed before the cycle it hangs off.
|
|
830
|
+
const inCycle = (agent) => {
|
|
831
|
+
const seen = new Set();
|
|
832
|
+
let current = agent;
|
|
833
|
+
while (current && !seen.has(current)) {
|
|
834
|
+
seen.add(current);
|
|
835
|
+
current = byName.get(parentOf.get(current));
|
|
836
|
+
if (current === agent) return true;
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
};
|
|
840
|
+
for (const agent of agents) {
|
|
841
|
+
if (!emitted.has(agent) && inCycle(agent)) emit(agent, 0, null);
|
|
842
|
+
}
|
|
843
|
+
// Belt and braces: nothing may be dropped, whatever shape the graph is in.
|
|
844
|
+
for (const agent of agents) emit(agent, 0, null);
|
|
845
|
+
return ordered;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Flatten the catalog into the VISIBLE row list the picker navigates.
|
|
850
|
+
*
|
|
851
|
+
* Section headings are items, not decoration: arrow keys walk onto them and
|
|
852
|
+
* Enter toggles them, so the whole dialog is reachable without a mouse. Rows
|
|
853
|
+
* inside a collapsed section are simply absent, which is what keeps
|
|
854
|
+
* `selectedIndex` a plain index into this array.
|
|
855
|
+
*/
|
|
856
|
+
/** Where the cursor starts. See the call site for why it is not just 0. */
|
|
857
|
+
export function agentPickerInitialIndex(items = []) {
|
|
858
|
+
const firstAgent = items.findIndex((item) => item.kind !== "section");
|
|
859
|
+
if (firstAgent >= 0) return firstAgent;
|
|
860
|
+
const firstClosed = items.findIndex((item) => item.kind === "section" && item.collapsed);
|
|
861
|
+
return firstClosed >= 0 ? firstClosed : 0;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export function buildAgentPickerItems(catalog = [], collapsedKeys = [], genericItem = null) {
|
|
865
|
+
const collapsed = new Set(collapsedKeys);
|
|
866
|
+
const items = [];
|
|
867
|
+
|
|
868
|
+
const builtins = catalog.filter((agent) => agent.builtin);
|
|
869
|
+
const packaged = catalog.filter((agent) => !agent.builtin);
|
|
870
|
+
|
|
871
|
+
// Generic sits ABOVE everything, outside both sections and outside every
|
|
872
|
+
// collapse. It is not an agent — it is the absence of one — and it is the
|
|
873
|
+
// most common pick, so it must never be a click away behind a twisty.
|
|
874
|
+
if (genericItem) items.push({ ...genericItem, depth: 0 });
|
|
875
|
+
|
|
876
|
+
if (builtins.length > 0) {
|
|
877
|
+
const builtinAgents = orderAgentsByComposition(builtins);
|
|
878
|
+
items.push({
|
|
879
|
+
id: `section:${AGENT_PICKER_BUILTIN_KEY}`,
|
|
880
|
+
kind: "section",
|
|
881
|
+
sectionKind: "builtin",
|
|
882
|
+
sectionKey: AGENT_PICKER_BUILTIN_KEY,
|
|
883
|
+
title: "Built-in",
|
|
884
|
+
meta: `${builtinAgents.length} agent${builtinAgents.length === 1 ? "" : "s"}`,
|
|
885
|
+
collapsed: collapsed.has(AGENT_PICKER_BUILTIN_KEY),
|
|
886
|
+
depth: 0,
|
|
887
|
+
});
|
|
888
|
+
if (!collapsed.has(AGENT_PICKER_BUILTIN_KEY)) {
|
|
889
|
+
// One level in from the header, so the section reads as a
|
|
890
|
+
// container rather than as a label floating above a flat list.
|
|
891
|
+
items.push(...builtinAgents.map((agent) => ({ ...agent, depth: agent.depth + 1 })));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// An empty category is omitted entirely rather than shown reading "0
|
|
896
|
+
// packages" — a heading whose only content is its own emptiness is noise.
|
|
897
|
+
for (const [sectionKey, title, belongs] of [
|
|
898
|
+
[AGENT_PICKER_SHARED_KEY, "Installed · Shared", (agent) => agent.group !== "mine"],
|
|
899
|
+
[AGENT_PICKER_MINE_KEY, "Installed · Yours", (agent) => agent.group === "mine"],
|
|
900
|
+
]) {
|
|
901
|
+
const members = packaged.filter(belongs);
|
|
902
|
+
if (members.length === 0) continue;
|
|
903
|
+
|
|
904
|
+
const groups = new Map();
|
|
905
|
+
for (const agent of members) {
|
|
906
|
+
const key = agentPickerPackageKey(agent);
|
|
907
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
908
|
+
groups.get(key).push(agent);
|
|
909
|
+
}
|
|
910
|
+
items.push({
|
|
911
|
+
id: `section:${sectionKey}`,
|
|
912
|
+
kind: "section",
|
|
913
|
+
sectionKind: "installed",
|
|
914
|
+
sectionKey,
|
|
915
|
+
title,
|
|
916
|
+
meta: `${groups.size} package${groups.size === 1 ? "" : "s"}`,
|
|
917
|
+
collapsed: collapsed.has(sectionKey),
|
|
918
|
+
depth: 0,
|
|
919
|
+
});
|
|
920
|
+
if (collapsed.has(sectionKey)) continue;
|
|
921
|
+
|
|
922
|
+
const keys = [...groups.keys()].sort((a, b) => {
|
|
923
|
+
const at = groups.get(a)[0];
|
|
924
|
+
const bt = groups.get(b)[0];
|
|
925
|
+
return String(at.packageTitle || at.packageName || "")
|
|
926
|
+
.localeCompare(String(bt.packageTitle || bt.packageName || ""));
|
|
927
|
+
});
|
|
928
|
+
for (const key of keys) {
|
|
929
|
+
const packageAgents = orderAgentsByComposition(groups.get(key));
|
|
930
|
+
const first = packageAgents[0] || {};
|
|
931
|
+
const entries = packageAgents.filter((agent) => agent.depth === 0).length;
|
|
932
|
+
items.push({
|
|
933
|
+
id: `section:${key}`,
|
|
934
|
+
kind: "section",
|
|
935
|
+
sectionKind: "package",
|
|
936
|
+
sectionKey: key,
|
|
937
|
+
title: first.packageTitle || first.packageName || "Package",
|
|
938
|
+
packageName: first.packageName || null,
|
|
939
|
+
packageSemver: first.packageSemver || null,
|
|
940
|
+
packageScope: first.packageScope || null,
|
|
941
|
+
ownerLabel: first.ownerLabel || null,
|
|
942
|
+
mine: first.group === "mine",
|
|
943
|
+
// No scope badge: the section it sits in already says which
|
|
944
|
+
// shelf this is, and repeating "[shared]" on every row under
|
|
945
|
+
// "Installed · Shared" is the kind of noise that makes a list
|
|
946
|
+
// harder to scan, not easier.
|
|
947
|
+
meta: `${entries} ${entries === 1 ? "entry" : "entries"} · ${packageAgents.length} agent${packageAgents.length === 1 ? "" : "s"}`,
|
|
948
|
+
collapsed: collapsed.has(key),
|
|
949
|
+
depth: 1,
|
|
950
|
+
});
|
|
951
|
+
if (collapsed.has(key)) continue;
|
|
952
|
+
// Package rows sit one level in from their section header, so
|
|
953
|
+
// their sub-agents start at depth 2.
|
|
954
|
+
items.push(...packageAgents.map((agent) => ({ ...agent, depth: agent.depth + 2 })));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return items;
|
|
959
|
+
}
|
|
960
|
+
|
|
716
961
|
function buildPromptAttachmentToken(filename) {
|
|
717
962
|
return `📎 ${String(filename || "").trim()}`;
|
|
718
963
|
}
|
|
@@ -1000,6 +1245,12 @@ function isBinaryPreview(filename, contentType = "") {
|
|
|
1000
1245
|
|| normalizedType === "application/wasm";
|
|
1001
1246
|
}
|
|
1002
1247
|
|
|
1248
|
+
// How recent a `session.artifact_presented` event must be for the portal to
|
|
1249
|
+
// act on it. Long enough to survive a slow turn and a brief reconnect; short
|
|
1250
|
+
// enough that a catch-up burst after a long disconnect does not reorganize the
|
|
1251
|
+
// workspace around something the agent said ages ago.
|
|
1252
|
+
const PRESENTED_ARTIFACT_FRESHNESS_MS = 120_000;
|
|
1253
|
+
|
|
1003
1254
|
function truncateFilePreview(content, limit = FILE_PREVIEW_CHAR_LIMIT) {
|
|
1004
1255
|
const text = String(content ?? "");
|
|
1005
1256
|
if (text.length <= limit) return text;
|
|
@@ -3952,6 +4203,32 @@ export class PilotSwarmUiController {
|
|
|
3952
4203
|
}
|
|
3953
4204
|
}
|
|
3954
4205
|
|
|
4206
|
+
/**
|
|
4207
|
+
* Act on a `session.artifact_presented` event from the live stream.
|
|
4208
|
+
*
|
|
4209
|
+
* Separate from mergeSessionEvent so the guards are testable on their own
|
|
4210
|
+
* and so the merge path stays a straight line. Returns whether it revealed.
|
|
4211
|
+
*/
|
|
4212
|
+
maybeRevealPresentedArtifact(sessionId, event) {
|
|
4213
|
+
const filename = String(event?.data?.filename || "").trim();
|
|
4214
|
+
if (!filename) return false;
|
|
4215
|
+
if (sessionId !== this.getState().sessions.activeSessionId) return false;
|
|
4216
|
+
|
|
4217
|
+
const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
|
|
4218
|
+
if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
|
|
4219
|
+
return false;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
// Same destination a user click reaches: the artifact reader, with the
|
|
4223
|
+
// chat still beside it. `fullscreen` from the tool means "give it the
|
|
4224
|
+
// whole window" and stays the phone/fullscreen path.
|
|
4225
|
+
const fullscreen = event?.data?.fullscreen === true;
|
|
4226
|
+
this.revealArtifact(sessionId, filename, fullscreen
|
|
4227
|
+
? { fullscreen: true }
|
|
4228
|
+
: { pane: true }).catch(() => {});
|
|
4229
|
+
return true;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
3955
4232
|
mergeSessionEvent(sessionId, event) {
|
|
3956
4233
|
if (!sessionId || !event) return false;
|
|
3957
4234
|
const state = this.getState();
|
|
@@ -3984,6 +4261,24 @@ export class PilotSwarmUiController {
|
|
|
3984
4261
|
},
|
|
3985
4262
|
});
|
|
3986
4263
|
}
|
|
4264
|
+
// show_artifact: the agent is presenting something to look at, so the
|
|
4265
|
+
// inspector switches to Files and opens that preview live.
|
|
4266
|
+
//
|
|
4267
|
+
// Three guards, because "the UI moves on its own" is only acceptable
|
|
4268
|
+
// when it is unmistakably a response to what is happening right now:
|
|
4269
|
+
//
|
|
4270
|
+
// - active session only. A background session finishing a dashboard
|
|
4271
|
+
// must never yank the pane away from the session being read.
|
|
4272
|
+
// - fresh events only. This path also runs for the catch-up burst
|
|
4273
|
+
// after a reconnect; replaying an hour-old presentation as if it
|
|
4274
|
+
// just happened would be a jump scare, not a feature.
|
|
4275
|
+
// - live path only. Bulk history loads (ensureSessionHistory) do not
|
|
4276
|
+
// come through here, so merely OPENING a session never auto-opens
|
|
4277
|
+
// whatever it last presented. Reopening is what the artifact link
|
|
4278
|
+
// in the transcript and the deep link are for.
|
|
4279
|
+
if (event.eventType === "session.artifact_presented") {
|
|
4280
|
+
this.maybeRevealPresentedArtifact(sessionId, event);
|
|
4281
|
+
}
|
|
3987
4282
|
this.reconcileOutboxAgainstEvent(sessionId, event);
|
|
3988
4283
|
const derivedModel = extractSessionModelFromEvent(event);
|
|
3989
4284
|
const currentSession = this.getState().sessions.byId[sessionId] || { sessionId };
|
|
@@ -4417,9 +4712,14 @@ export class PilotSwarmUiController {
|
|
|
4417
4712
|
const activeSession = state.sessions?.activeSessionId
|
|
4418
4713
|
? state.sessions.byId[state.sessions.activeSessionId]
|
|
4419
4714
|
: null;
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4715
|
+
// ONLY when the selected row is the folder itself. Selecting a folder
|
|
4716
|
+
// and pressing New is the user pointing at it; inheriting the group
|
|
4717
|
+
// from a selected MEMBER is not — it filed new sessions inside
|
|
4718
|
+
// whichever folder happened to hold the last session read, so a new
|
|
4719
|
+
// session appeared buried mid-list instead of at the end where the
|
|
4720
|
+
// sort puts an unplaced row.
|
|
4721
|
+
if (!activeSession?.isGroup) return options;
|
|
4722
|
+
const groupId = activeSession.groupId || sessionGroupIdFromRowId(activeSession.sessionId);
|
|
4423
4723
|
return groupId ? { ...options, groupId } : options;
|
|
4424
4724
|
}
|
|
4425
4725
|
|
|
@@ -4503,9 +4803,9 @@ export class PilotSwarmUiController {
|
|
|
4503
4803
|
return;
|
|
4504
4804
|
}
|
|
4505
4805
|
|
|
4506
|
-
//
|
|
4507
|
-
//
|
|
4508
|
-
//
|
|
4806
|
+
// The CATALOG (every agent, section-agnostic). The visible row list is
|
|
4807
|
+
// derived from it by buildAgentPickerItems, which is re-run whenever a
|
|
4808
|
+
// section is expanded or collapsed.
|
|
4509
4809
|
const agentItems = [];
|
|
4510
4810
|
for (const agent of agents) {
|
|
4511
4811
|
const agentName = String(agent?.name || "").trim();
|
|
@@ -4533,17 +4833,24 @@ export class PilotSwarmUiController {
|
|
|
4533
4833
|
? "mine"
|
|
4534
4834
|
: "shared",
|
|
4535
4835
|
packageName: agent?.packageName || null,
|
|
4836
|
+
packageTitle: agent?.packageTitle || null,
|
|
4536
4837
|
packageSemver: agent?.packageSemver || null,
|
|
4537
4838
|
packageScope: agent?.scope || null,
|
|
4839
|
+
ownerLabel: agent?.ownerLabel || null,
|
|
4840
|
+
startedBy: Array.isArray(agent?.startedBy) ? agent.startedBy.filter(Boolean) : [],
|
|
4841
|
+
// RAW: undefined means "not declared". The default is applied
|
|
4842
|
+
// in orderAgentsByComposition, which is the only place that
|
|
4843
|
+
// knows whether a creator actually resolved.
|
|
4844
|
+
supportsDirectStart: typeof agent?.supportsDirectStart === "boolean" ? agent.supportsDirectStart : undefined,
|
|
4538
4845
|
builtin: agent?.source !== "package",
|
|
4539
4846
|
});
|
|
4540
4847
|
}
|
|
4541
|
-
const
|
|
4848
|
+
const catalog = [
|
|
4542
4849
|
...agentItems.filter((item) => item.group === "shared"),
|
|
4543
4850
|
...agentItems.filter((item) => item.group === "mine"),
|
|
4544
4851
|
];
|
|
4545
|
-
|
|
4546
|
-
|
|
4852
|
+
const genericItem = allowGeneric
|
|
4853
|
+
? {
|
|
4547
4854
|
id: "__generic__",
|
|
4548
4855
|
kind: "generic",
|
|
4549
4856
|
group: "generic",
|
|
@@ -4553,16 +4860,39 @@ export class PilotSwarmUiController {
|
|
|
4553
4860
|
splash: null,
|
|
4554
4861
|
splashMobile: null,
|
|
4555
4862
|
initialPrompt: null,
|
|
4556
|
-
|
|
4557
|
-
|
|
4863
|
+
supportsDirectStart: true,
|
|
4864
|
+
}
|
|
4865
|
+
: null;
|
|
4866
|
+
|
|
4867
|
+
// EVERYTHING starts closed. The dialog opens as a short menu of
|
|
4868
|
+
// categories — Generic, then whichever of Built-in / Shared / Yours
|
|
4869
|
+
// this deployment actually has — and you open the one you want. A
|
|
4870
|
+
// section expanded by default is a section every other user has to
|
|
4871
|
+
// scroll past.
|
|
4872
|
+
const collapsed = [
|
|
4873
|
+
AGENT_PICKER_BUILTIN_KEY,
|
|
4874
|
+
AGENT_PICKER_SHARED_KEY,
|
|
4875
|
+
AGENT_PICKER_MINE_KEY,
|
|
4876
|
+
...new Set(catalog.filter((item) => !item.builtin).map((item) => agentPickerPackageKey(item))),
|
|
4877
|
+
];
|
|
4878
|
+
const items = buildAgentPickerItems(catalog, collapsed, genericItem);
|
|
4558
4879
|
|
|
4559
4880
|
this.dispatch({
|
|
4560
4881
|
type: "ui/modal",
|
|
4561
4882
|
modal: {
|
|
4562
4883
|
type: "sessionAgentPicker",
|
|
4563
4884
|
title: "Select agent for new session",
|
|
4885
|
+
catalog,
|
|
4886
|
+
genericItem,
|
|
4887
|
+
collapsed,
|
|
4564
4888
|
items,
|
|
4565
|
-
|
|
4889
|
+
// Land somewhere Enter does something useful. First choice is a
|
|
4890
|
+
// real agent row; with none visible (no generic, no baked
|
|
4891
|
+
// agents, every package closed) fall back to the first CLOSED
|
|
4892
|
+
// package, whose Enter opens it. Landing on an open section
|
|
4893
|
+
// header — which findIndex's -1 used to do via Math.max — made
|
|
4894
|
+
// the very first keystroke collapse the whole list.
|
|
4895
|
+
selectedIndex: agentPickerInitialIndex(items),
|
|
4566
4896
|
previousFocus,
|
|
4567
4897
|
sessionOptions: options,
|
|
4568
4898
|
},
|
|
@@ -4838,10 +5168,16 @@ export class PilotSwarmUiController {
|
|
|
4838
5168
|
}
|
|
4839
5169
|
|
|
4840
5170
|
openThemePicker() {
|
|
5171
|
+
// listThemes() is already ordered by group then label, so the picker
|
|
5172
|
+
// renders a heading wherever `group` changes rather than re-sorting.
|
|
5173
|
+
// The list itself stays FLAT: headings are drawn between rows, never
|
|
5174
|
+
// as rows, so selectedIndex keeps addressing themes and arrow-key
|
|
5175
|
+
// navigation never lands on a heading.
|
|
4841
5176
|
const themes = listThemes().map((theme) => ({
|
|
4842
5177
|
id: theme.id,
|
|
4843
5178
|
label: theme.label,
|
|
4844
5179
|
description: theme.description,
|
|
5180
|
+
group: theme.group,
|
|
4845
5181
|
page: theme.page,
|
|
4846
5182
|
terminal: theme.terminal,
|
|
4847
5183
|
tui: theme.tui,
|
|
@@ -4978,7 +5314,11 @@ export class PilotSwarmUiController {
|
|
|
4978
5314
|
* READ. Switches the inspector to Files, selects the artifact, and warms
|
|
4979
5315
|
* the preview so the pane is populated by the time it renders.
|
|
4980
5316
|
*/
|
|
4981
|
-
async revealArtifact(sessionId, filename, {
|
|
5317
|
+
async revealArtifact(sessionId, filename, {
|
|
5318
|
+
force = true,
|
|
5319
|
+
fullscreen = false,
|
|
5320
|
+
pane = false,
|
|
5321
|
+
} = {}) {
|
|
4982
5322
|
const resolvedSessionId = sessionId || this.getState().sessions.activeSessionId;
|
|
4983
5323
|
const name = String(filename || "").trim();
|
|
4984
5324
|
if (!resolvedSessionId || !name) return false;
|
|
@@ -4994,12 +5334,37 @@ export class PilotSwarmUiController {
|
|
|
4994
5334
|
if (this.getState().sessions.activeSessionId !== resolvedSessionId) {
|
|
4995
5335
|
await this.loadSession(resolvedSessionId).catch(() => null);
|
|
4996
5336
|
}
|
|
4997
|
-
|
|
4998
|
-
//
|
|
4999
|
-
//
|
|
5000
|
-
//
|
|
5001
|
-
//
|
|
5002
|
-
|
|
5337
|
+
// The reader REPLACES the inspector, so it has no business changing
|
|
5338
|
+
// which inspector tab is selected. Switching to Files here meant that
|
|
5339
|
+
// opening an artifact from a session you were watching on the Sequence
|
|
5340
|
+
// tab silently reassigned that tab, and ✕ dropped you on a file list
|
|
5341
|
+
// you never opened. Leaving it alone makes ✕ restore the tab for free,
|
|
5342
|
+
// with no state to capture. The non-pane paths still need Files,
|
|
5343
|
+
// because there the inspector IS the preview.
|
|
5344
|
+
if (!pane) this.dispatch({ type: "ui/inspectorTab", inspectorTab: "files" });
|
|
5345
|
+
// The takeover pane is its own focus surface and, unlike the inspector,
|
|
5346
|
+
// it is what the user is looking at — so it must NOT steal focus to the
|
|
5347
|
+
// artifact list underneath. Chat keeps focus, which is the point: you
|
|
5348
|
+
// followed a link from the transcript and should still be able to type.
|
|
5349
|
+
if (pane) {
|
|
5350
|
+
// Whether the column was already collapsed is read HERE rather than
|
|
5351
|
+
// taken from the caller, so every entry point — a chat card, a deep
|
|
5352
|
+
// link, an agent's show_artifact — gets the same answer.
|
|
5353
|
+
//
|
|
5354
|
+
// And if it WAS collapsed, the column has to be opened: rendering
|
|
5355
|
+
// the reader into a zero-width track produced a pane that existed
|
|
5356
|
+
// in the DOM, reported itself open, and could not be seen. Closing
|
|
5357
|
+
// collapses it again, so the detour is invisible.
|
|
5358
|
+
const wasHidden = Boolean(this.getCurrentLayout()?.rightHidden);
|
|
5359
|
+
if (wasHidden) this.expandRightColumn();
|
|
5360
|
+
this.dispatch({ type: "files/pane", open: true, restoresToHidden: wasHidden });
|
|
5361
|
+
} else {
|
|
5362
|
+
// Focus the inspector, not just the tab. On desktop this makes j/k
|
|
5363
|
+
// drive the artifact list immediately; on mobile the visible pane is
|
|
5364
|
+
// derived from focusRegion, so without this the preview would open on
|
|
5365
|
+
// a pane the user cannot see.
|
|
5366
|
+
this.setFocus("inspector");
|
|
5367
|
+
}
|
|
5003
5368
|
await this.ensureFilesForSession(resolvedSessionId, { force }).catch(() => null);
|
|
5004
5369
|
this.dispatch({ type: "files/select", sessionId: resolvedSessionId, filename: name });
|
|
5005
5370
|
await this.ensureFilePreview(resolvedSessionId, name, { force }).catch(() => null);
|
|
@@ -5007,6 +5372,57 @@ export class PilotSwarmUiController {
|
|
|
5007
5372
|
return true;
|
|
5008
5373
|
}
|
|
5009
5374
|
|
|
5375
|
+
/**
|
|
5376
|
+
* Close the artifact takeover pane, restoring the right column.
|
|
5377
|
+
*
|
|
5378
|
+
* Restores the inspector/activity split it replaced — unless the column was
|
|
5379
|
+
* already collapsed when the pane opened, in which case it collapses again.
|
|
5380
|
+
* Opening a reader must not become a way to un-hide panes the user had
|
|
5381
|
+
* deliberately resized away.
|
|
5382
|
+
*/
|
|
5383
|
+
async closeArtifactPane() {
|
|
5384
|
+
const restoresToHidden = this.getState().files.paneRestoresToHidden;
|
|
5385
|
+
this.dispatch({ type: "files/pane", open: false });
|
|
5386
|
+
this.dispatch({ type: "files/fullscreen", fullscreen: false });
|
|
5387
|
+
// Selection is deliberately left alone: the artifact stays selected in
|
|
5388
|
+
// the Files list, so reopening the column shows what you were reading
|
|
5389
|
+
// rather than an empty preview.
|
|
5390
|
+
this.dispatch({ type: "files/previewOrigin", origin: null });
|
|
5391
|
+
if (restoresToHidden) this.collapseRightColumn();
|
|
5392
|
+
this.dispatch({ type: "ui/focus", focusRegion: FOCUS_REGIONS.CHAT });
|
|
5393
|
+
return { collapsedRightColumn: Boolean(restoresToHidden) };
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5396
|
+
/**
|
|
5397
|
+
* Drive the left/right split far enough right that the layout's own
|
|
5398
|
+
* collapse rule hides the right column.
|
|
5399
|
+
*
|
|
5400
|
+
* Expressed as a paneAdjust rather than a new "hidden" flag so there stays
|
|
5401
|
+
* exactly one source of truth for column width — the same number the
|
|
5402
|
+
* resizer writes. A second flag would let the two disagree.
|
|
5403
|
+
*/
|
|
5404
|
+
/** Return the left/right split to its default, un-collapsing the column. */
|
|
5405
|
+
expandRightColumn() {
|
|
5406
|
+
this.dispatch({ type: "ui/paneAdjust", paneAdjust: 0 });
|
|
5407
|
+
const nextLayout = this.getCurrentLayout({ paneAdjust: 0 });
|
|
5408
|
+
const currentFocus = this.getState().ui.focusRegion;
|
|
5409
|
+
const safeFocus = normalizeFocusRegion(currentFocus, nextLayout);
|
|
5410
|
+
if (safeFocus !== currentFocus) this.setFocus(safeFocus);
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
collapseRightColumn() {
|
|
5414
|
+
const layoutState = this.getState().ui.layout || {};
|
|
5415
|
+
const totalWidth = layoutState.viewportWidth ?? 120;
|
|
5416
|
+
const baseLeftWidth = Math.floor(totalWidth * DEFAULT_LEFT_PANE_RATIO);
|
|
5417
|
+
// One past the threshold: `<=` collapses, so land strictly inside it.
|
|
5418
|
+
const paneAdjust = totalWidth - COLLAPSE_RIGHT_THRESHOLD - baseLeftWidth;
|
|
5419
|
+
this.dispatch({ type: "ui/paneAdjust", paneAdjust });
|
|
5420
|
+
const nextLayout = this.getCurrentLayout({ paneAdjust });
|
|
5421
|
+
const currentFocus = this.getState().ui.focusRegion;
|
|
5422
|
+
const safeFocus = normalizeFocusRegion(currentFocus, nextLayout);
|
|
5423
|
+
if (safeFocus !== currentFocus) this.setFocus(safeFocus);
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5010
5426
|
/**
|
|
5011
5427
|
* Leave the artifact preview, returning wherever the user came FROM.
|
|
5012
5428
|
*
|
|
@@ -5328,7 +5744,9 @@ export class PilotSwarmUiController {
|
|
|
5328
5744
|
type: "ui/modal",
|
|
5329
5745
|
modal: {
|
|
5330
5746
|
type: "renameSession",
|
|
5331
|
-
|
|
5747
|
+
// A group row's id is synthetic, so showing its short form
|
|
5748
|
+
// reads as a stray hex string rather than an identifier.
|
|
5749
|
+
title: session.isGroup ? "Rename folder" : `Rename (${shortSessionIdValue(sessionId)})`,
|
|
5332
5750
|
sessionId,
|
|
5333
5751
|
previousFocus: state.ui.focusRegion,
|
|
5334
5752
|
value,
|
|
@@ -5336,13 +5754,19 @@ export class PilotSwarmUiController {
|
|
|
5336
5754
|
agentTitlePrefix,
|
|
5337
5755
|
currentTitle: String(session.title || "").trim(),
|
|
5338
5756
|
maxLength,
|
|
5757
|
+
// Folders share this modal but not its caveats: a group has no
|
|
5758
|
+
// agent and no LLM-generated title, so the selector suppresses
|
|
5759
|
+
// the auto-title warning for them.
|
|
5760
|
+
isGroup: Boolean(session.isGroup),
|
|
5339
5761
|
},
|
|
5340
5762
|
});
|
|
5341
5763
|
this.dispatch({
|
|
5342
5764
|
type: "ui/status",
|
|
5343
|
-
text:
|
|
5344
|
-
?
|
|
5345
|
-
:
|
|
5765
|
+
text: session.isGroup
|
|
5766
|
+
? "Type a new folder name and press Enter to save"
|
|
5767
|
+
: agentTitlePrefix
|
|
5768
|
+
? `Rename title for ${agentTitlePrefix}; the agent-name prefix stays fixed`
|
|
5769
|
+
: "Type a new session title and press Enter to save",
|
|
5346
5770
|
});
|
|
5347
5771
|
}
|
|
5348
5772
|
|
|
@@ -5839,6 +6263,72 @@ export class PilotSwarmUiController {
|
|
|
5839
6263
|
this.dispatch({ type: "ui/status", text: "Connected" });
|
|
5840
6264
|
}
|
|
5841
6265
|
|
|
6266
|
+
/**
|
|
6267
|
+
* Open or close one agent-picker section.
|
|
6268
|
+
*
|
|
6269
|
+
* The visible row list is rebuilt from the catalog, and selection is
|
|
6270
|
+
* carried by ITEM ID rather than index: collapsing a section above the
|
|
6271
|
+
* cursor shifts every index below it, and re-using the old number would
|
|
6272
|
+
* silently move the highlight onto a different agent.
|
|
6273
|
+
*/
|
|
6274
|
+
toggleAgentPickerSection(sectionKey, force = null) {
|
|
6275
|
+
const modal = this.getState().ui.modal;
|
|
6276
|
+
if (!modal || modal.type !== "sessionAgentPicker" || !sectionKey) return;
|
|
6277
|
+
const collapsed = new Set(modal.collapsed || []);
|
|
6278
|
+
const shouldCollapse = force === null ? !collapsed.has(sectionKey) : force;
|
|
6279
|
+
if (shouldCollapse === collapsed.has(sectionKey)) return;
|
|
6280
|
+
if (shouldCollapse) collapsed.add(sectionKey);
|
|
6281
|
+
else collapsed.delete(sectionKey);
|
|
6282
|
+
|
|
6283
|
+
const nextCollapsed = [...collapsed];
|
|
6284
|
+
const items = buildAgentPickerItems(modal.catalog || [], nextCollapsed, modal.genericItem || null);
|
|
6285
|
+
const selectedId = modal.items?.[modal.selectedIndex || 0]?.id;
|
|
6286
|
+
let nextIndex = items.findIndex((item) => item.id === selectedId);
|
|
6287
|
+
if (nextIndex < 0) {
|
|
6288
|
+
// The selected row was inside the section that just closed — land
|
|
6289
|
+
// on that section's own header, which is where it went.
|
|
6290
|
+
nextIndex = Math.max(0, items.findIndex((item) => item.sectionKey === sectionKey));
|
|
6291
|
+
}
|
|
6292
|
+
this.dispatch({
|
|
6293
|
+
type: "ui/modal",
|
|
6294
|
+
modal: { ...modal, collapsed: nextCollapsed, items, selectedIndex: nextIndex },
|
|
6295
|
+
});
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
/**
|
|
6299
|
+
* Left/right on the agent picker. Right opens a closed section (or steps
|
|
6300
|
+
* into it); left closes an open one, or jumps to the parent header when
|
|
6301
|
+
* the cursor is already on a leaf — the same shape as the session tree.
|
|
6302
|
+
*/
|
|
6303
|
+
moveAgentPickerSection(delta) {
|
|
6304
|
+
const modal = this.getState().ui.modal;
|
|
6305
|
+
if (!modal || modal.type !== "sessionAgentPicker") return false;
|
|
6306
|
+
const items = Array.isArray(modal.items) ? modal.items : [];
|
|
6307
|
+
const index = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, items.length - 1));
|
|
6308
|
+
const item = items[index];
|
|
6309
|
+
if (!item) return false;
|
|
6310
|
+
|
|
6311
|
+
if (delta > 0) {
|
|
6312
|
+
if (item.kind === "section" && item.collapsed) {
|
|
6313
|
+
this.toggleAgentPickerSection(item.sectionKey, false);
|
|
6314
|
+
return true;
|
|
6315
|
+
}
|
|
6316
|
+
return false;
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
if (item.kind === "section" && !item.collapsed) {
|
|
6320
|
+
this.toggleAgentPickerSection(item.sectionKey, true);
|
|
6321
|
+
return true;
|
|
6322
|
+
}
|
|
6323
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
6324
|
+
if (items[i].kind === "section" && items[i].depth < item.depth) {
|
|
6325
|
+
this.dispatch({ type: "ui/modalSelection", index: i });
|
|
6326
|
+
return true;
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
return false;
|
|
6330
|
+
}
|
|
6331
|
+
|
|
5842
6332
|
moveModalSelection(delta) {
|
|
5843
6333
|
const modal = this.getState().ui.modal;
|
|
5844
6334
|
if (!modal) return;
|
|
@@ -5883,6 +6373,10 @@ export class PilotSwarmUiController {
|
|
|
5883
6373
|
|
|
5884
6374
|
moveModalPane(delta) {
|
|
5885
6375
|
const modal = this.getState().ui.modal;
|
|
6376
|
+
if (modal?.type === "sessionAgentPicker") {
|
|
6377
|
+
this.moveAgentPickerSection(delta);
|
|
6378
|
+
return;
|
|
6379
|
+
}
|
|
5886
6380
|
if (!modal || (modal.type !== "logFilter" && modal.type !== "filesFilter" && modal.type !== "historyFormat") || !Array.isArray(modal.items) || modal.items.length === 0) return;
|
|
5887
6381
|
const current = Math.max(0, Number(modal.selectedIndex) || 0);
|
|
5888
6382
|
const next = (current + delta + modal.items.length) % modal.items.length;
|
|
@@ -6052,6 +6546,21 @@ export class PilotSwarmUiController {
|
|
|
6052
6546
|
}
|
|
6053
6547
|
if (modal.type === "sessionAgentPicker") {
|
|
6054
6548
|
const item = modal.items?.[modal.selectedIndex || 0];
|
|
6549
|
+
// Enter on a heading opens or closes it; the dialog stays up.
|
|
6550
|
+
if (item?.kind === "section") {
|
|
6551
|
+
this.toggleAgentPickerSection(item.sectionKey);
|
|
6552
|
+
return;
|
|
6553
|
+
}
|
|
6554
|
+
// A called-only agent is rendered so the package's composition is
|
|
6555
|
+
// visible, but starting one cold is not a thing you can do. Refuse
|
|
6556
|
+
// here rather than letting the create fail after the dialog closes.
|
|
6557
|
+
if (item && item.kind !== "generic" && item.supportsDirectStart === false) {
|
|
6558
|
+
this.dispatch({
|
|
6559
|
+
type: "ui/status",
|
|
6560
|
+
text: `${item.title || item.agentName} is created by another agent and cannot be started on its own`,
|
|
6561
|
+
});
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6055
6564
|
const previousFocus = modal.previousFocus;
|
|
6056
6565
|
const sessionOptions = modal.sessionOptions || {};
|
|
6057
6566
|
this.dispatch({ type: "ui/modal", modal: null });
|