pilotswarm 0.5.30 → 0.5.32
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/node-sdk-transport.js +4 -0
- package/ui/core/src/controller.js +102 -30
- package/ui/core/src/reducer.js +50 -1
- package/ui/core/src/selectors.js +68 -22
- package/ui/core/src/state.js +2 -0
- package/ui/core/src/themes/doom.js +106 -0
- package/ui/core/src/themes/helpers.js +7 -1
- package/ui/core/src/themes/index.js +4 -0
- package/ui/core/src/themes/winamp.js +104 -0
- package/ui/react/src/components.js +25 -0
- package/ui/react/src/web-app.js +320 -51
- package/web/api/ws.js +7 -0
- package/web/authz.js +14 -81
- package/web/dist/assets/{abnfDiagram-VRR7QNED-Cimc0f-u.js → abnfDiagram-VRR7QNED-mXFQ4a3c.js} +1 -1
- package/web/dist/assets/{arc-D8-_Dx5J.js → arc-BPuuvbge.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-lzMAuQHJ.js → architectureDiagram-ZJ3FMSHR-Dt4uUYIe.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Dp2CWjVS.js → blockDiagram-677ZJIJ3-Ax8Q22vV.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DF93UkOy.js → c4Diagram-LMCZKHZV-CPiukyHt.js} +1 -1
- package/web/dist/assets/channel-BCzDctYg.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-pbsQRM-F.js → chunk-2Q5K7J3B-VJLqCZJX.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-B_f-gG1O.js → chunk-32BRIVSS-DqOshxhf.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-gqlpJDmt.js → chunk-5VM5RSS4-KPqV21jV.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-Cms7hPmM.js → chunk-EX3LRPZG-DfhiCVl_.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Boq5PtPI.js → chunk-JWPE2WC7-H6khJPuW.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-DVn9i8zt.js → chunk-MOJQB5TN-BKMAzQxj.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-a1vKs3UX.js → chunk-RYQCIY6F-DNXRTxtj.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-i60p5fHd.js → chunk-V7JOEXUC-B56PM6Gv.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-a8p0o-8B.js → chunk-VR4S4FIN-Boi3hGkB.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BwP1yBKd.js → chunk-XXDRQBXY-DmJyn890.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-ChqjVZil.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-ChqjVZil.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-HW6kGgze.js → cose-bilkent-JH36ORCC-BRdFlFWt.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-YUFMn-vE.js → cynefin-VYW2F7L2-DqE-xib2.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Brdy17i4.js → cynefinDiagram-TSTJHNR4-D-QBZjIL.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-CAiSJJj-.js → dagre-VKFMJZFB-DYu9oQV6.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-B63Cqe0J.js → diagram-FQU43EPY-DOwYccCg.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-ByktrDJ2.js → diagram-G47NLZAW-DTSEmrPr.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-BLSfRMoE.js → diagram-NH7WQ7WH-OqbzzquA.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DNVu-zpk.js → diagram-OA4YK3LP-BMVG6A8I.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-CNf4Sz-I.js → diagram-WEI45ONY-Rn2Q5xaz.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-9CT3ergx.js → ebnfDiagram-CCIWWBDH-aL32_v5y.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-BPJHAMgl.js → erDiagram-Q63AITRT-DKCpyczx.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BVtpr6Iu.js → flowDiagram-23GEKE2U-CrZTPgR_.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-BTHnv9sV.js → ganttDiagram-NO4QXBWP-DnSE1OtC.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BfmtqbL5.js → gitGraphDiagram-IHSO6WYX-C5SeYPUR.js} +1 -1
- package/web/dist/assets/{index-Bmb8qK39.js → index-B-TEKrRB.js} +6 -6
- package/web/dist/assets/index-D2B5bzSN.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-kU4i4HRu.js → infoDiagram-FWYZ7A6U-DDnJZp0X.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-Ch7i-E8O.js → ishikawaDiagram-FXEZZL3T-ewhzfFkq.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-CPQxp0ps.js → journeyDiagram-5HDEW3XC-T0iWWkLM.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-BBgMVqoJ.js → kanban-definition-HUTT4EX6-D1MFQZjb.js} +1 -1
- package/web/dist/assets/{linear-rwksvzXJ.js → linear-BKfPK9wE.js} +1 -1
- package/web/dist/assets/{mermaid.core-o7PszRUc.js → mermaid.core-CyzD7dTc.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-wgOTdzlq.js → mindmap-definition-LN4V7U3C-CWXtcTlP.js} +1 -1
- package/web/dist/assets/{papaparse.min-BRXtjEI6.js → papaparse.min-DLUoV5k5.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BkhQqqTm.js → pegDiagram-2B236MQR-BFjZun-r.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-CcMnvxUz.js → pieDiagram-ENE6RG2P-DMDtW4g7.js} +1 -1
- package/web/dist/assets/pilotswarm-DA0u7Cq_.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-_KD1QeEr.js → quadrantDiagram-ABIIQ3AL-CAwPVaYL.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-CqI0olIe.js → railroadDiagram-RFXS5EU6-Co102PkQ.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-1JxRwusF.js → requirementDiagram-TGXJPOKE-D19VlRSl.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-9IpFoKzi.js → sankeyDiagram-HTMAVEWB-Sd_a3ukL.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-teyObSQ9.js → sequenceDiagram-DBY2YBRQ-CojDRK1c.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DVhU2J_6.js → sizeCapture-X5ZJPWSS-XqXcxnDo.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-DHPoWpIQ.js → stateDiagram-2N3HPSRC-BcN_w699.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BV_w1D3C.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BxTtDQoB.js → swimlanes-5IMT3BWC-BcO6ai_I.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BnYdP0qP.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-D0wXZx-4.js → timeline-definition-FHXFAJF6-DOPfYxUq.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BSFyI3iV.js → vennDiagram-L72KCM5P-BSKLkEOC.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BN-S7hh0.js → wardleyDiagram-EHGQE667-BdcYAVw2.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-nDNuPs7o.js → xychartDiagram-FW5EYKEG-C6EJT4jB.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +55 -0
- package/web/server.js +7 -0
- package/web/dist/assets/channel-OwWeiCox.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CDaP7nuC.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CDaP7nuC.js +0 -1
- package/web/dist/assets/index-B37T1c2s.css +0 -1
- package/web/dist/assets/pilotswarm-DYXtzuUN.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DJjZWYLS.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CV12vDcK.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.32",
|
|
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.32",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
@@ -1172,6 +1172,10 @@ export class NodeSdkTransport {
|
|
|
1172
1172
|
return this.mgmt.recordAuthzAudit(entry);
|
|
1173
1173
|
}
|
|
1174
1174
|
|
|
1175
|
+
async recordUserRole(principal, role) {
|
|
1176
|
+
return this.mgmt.recordUserRole(principal, role);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1175
1179
|
async listAuthzAudit(opts) {
|
|
1176
1180
|
return this.mgmt.listAuthzAudit(opts);
|
|
1177
1181
|
}
|
|
@@ -2814,6 +2814,29 @@ export class PilotSwarmUiController {
|
|
|
2814
2814
|
// the current package's workspace (stale-response race).
|
|
2815
2815
|
if (this.getState().admin?.packages?.selectedName !== name) return;
|
|
2816
2816
|
this.dispatch({ type: "admin/packages/tree/loaded", name, tree });
|
|
2817
|
+
// The package's own CHANGELOG, when it ships one. Loaded
|
|
2818
|
+
// alongside the tree rather than on demand because it is
|
|
2819
|
+
// the first thing a reviewer wants: what changed, why, and
|
|
2820
|
+
// whether an agent or a human authored it.
|
|
2821
|
+
const hasChangelog = tree?.files?.some((f) => f.path === "CHANGELOG.md");
|
|
2822
|
+
if (hasChangelog && typeof this.transport.getAgentPackageFile === "function") {
|
|
2823
|
+
void (async () => {
|
|
2824
|
+
try {
|
|
2825
|
+
const file = await this.transport.getAgentPackageFile(name, null, "CHANGELOG.md");
|
|
2826
|
+
if (this.getState().admin?.packages?.selectedName !== name) return;
|
|
2827
|
+
this.dispatch({
|
|
2828
|
+
type: "admin/packages/changelog/loaded",
|
|
2829
|
+
name,
|
|
2830
|
+
content: typeof file === "string" ? file : (file?.content ?? file?.text ?? ""),
|
|
2831
|
+
});
|
|
2832
|
+
} catch {
|
|
2833
|
+
// A missing/unreadable changelog is not an error
|
|
2834
|
+
// worth interrupting package browsing for.
|
|
2835
|
+
}
|
|
2836
|
+
})();
|
|
2837
|
+
} else {
|
|
2838
|
+
this.dispatch({ type: "admin/packages/changelog/loaded", name, content: null });
|
|
2839
|
+
}
|
|
2817
2840
|
// Default preview: plugin.json (always present in a valid package).
|
|
2818
2841
|
const first = tree?.files?.find((f) => f.path === "plugin.json") ?? tree?.files?.[0];
|
|
2819
2842
|
if (first) void this.selectAdminPackageFile(first.path);
|
|
@@ -4422,16 +4445,53 @@ export class PilotSwarmUiController {
|
|
|
4422
4445
|
this.dispatch({ type: "ui/chatViewMode", mode });
|
|
4423
4446
|
}
|
|
4424
4447
|
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4448
|
+
/**
|
|
4449
|
+
* Opens the agent step of the new-session flow.
|
|
4450
|
+
*
|
|
4451
|
+
* This step needs a network round trip (listCreatableAgents) before it can
|
|
4452
|
+
* render, and it is reached from the middle of a modal chain. Callers used
|
|
4453
|
+
* to close the current step BEFORE awaiting it, which left the overlay
|
|
4454
|
+
* unmounted for the length of that fetch — the dialog visibly blinked out
|
|
4455
|
+
* and back on the way to the agent list.
|
|
4456
|
+
*
|
|
4457
|
+
* So nothing is dismissed until we know what replaces it: on success the
|
|
4458
|
+
* new modal REPLACES the open one in a single dispatch and the overlay
|
|
4459
|
+
* never leaves the screen; only the paths that open no modal at all close
|
|
4460
|
+
* it. `previousFocus` is threaded from the previous step because that step
|
|
4461
|
+
* is deliberately still up, so it cannot be recovered from focus state.
|
|
4462
|
+
*/
|
|
4463
|
+
async openSessionAgentPicker(options = {}, previousFocusOverride = null) {
|
|
4464
|
+
const previousFocus = previousFocusOverride ?? this.getState().ui.focusRegion;
|
|
4465
|
+
const dismiss = () => {
|
|
4466
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
4467
|
+
if (previousFocus) {
|
|
4468
|
+
this.setFocus(previousFocus);
|
|
4469
|
+
}
|
|
4470
|
+
};
|
|
4471
|
+
|
|
4472
|
+
let agents = [];
|
|
4473
|
+
try {
|
|
4474
|
+
agents = typeof this.transport.listCreatableAgents === "function"
|
|
4475
|
+
? await this.transport.listCreatableAgents()
|
|
4476
|
+
: [];
|
|
4477
|
+
} catch (error) {
|
|
4478
|
+
// The previous step is deliberately still on screen. If the fetch
|
|
4479
|
+
// fails we own dismissing it — otherwise the flow strands on a step
|
|
4480
|
+
// whose Enter key has already been consumed.
|
|
4481
|
+
dismiss();
|
|
4482
|
+
this.dispatch({
|
|
4483
|
+
type: "ui/status",
|
|
4484
|
+
text: `Could not load agents: ${error?.message || error}`,
|
|
4485
|
+
});
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4429
4488
|
const sessionPolicy = typeof this.transport.getSessionCreationPolicy === "function"
|
|
4430
4489
|
? this.transport.getSessionCreationPolicy()
|
|
4431
4490
|
: null;
|
|
4432
4491
|
const allowGeneric = sessionPolicy?.creation?.allowGeneric ?? true;
|
|
4433
4492
|
|
|
4434
4493
|
if (!Array.isArray(agents) || agents.length === 0) {
|
|
4494
|
+
dismiss();
|
|
4435
4495
|
if (!allowGeneric) {
|
|
4436
4496
|
this.dispatch({
|
|
4437
4497
|
type: "ui/status",
|
|
@@ -4503,7 +4563,7 @@ export class PilotSwarmUiController {
|
|
|
4503
4563
|
title: "Select agent for new session",
|
|
4504
4564
|
items,
|
|
4505
4565
|
selectedIndex: 0,
|
|
4506
|
-
previousFocus
|
|
4566
|
+
previousFocus,
|
|
4507
4567
|
sessionOptions: options,
|
|
4508
4568
|
},
|
|
4509
4569
|
});
|
|
@@ -4526,13 +4586,17 @@ export class PilotSwarmUiController {
|
|
|
4526
4586
|
await this.openSessionAgentPicker(options);
|
|
4527
4587
|
}
|
|
4528
4588
|
|
|
4529
|
-
|
|
4589
|
+
// previousFocus rides along the whole chain: each step is left on screen
|
|
4590
|
+
// until its successor is ready (see openSessionAgentPicker), so a later
|
|
4591
|
+
// step cannot read the pre-modal focus back off the state.
|
|
4592
|
+
async openReasoningEffortPicker(modelItem, sessionOptions = {}, previousFocusOverride = null) {
|
|
4593
|
+
const previousFocus = previousFocusOverride ?? this.getState().ui.focusRegion;
|
|
4530
4594
|
const supported = normalizeReasoningEfforts(modelItem?.supportedReasoningEfforts);
|
|
4531
4595
|
const selectedEffort = sessionOptions?.reasoningEffort || resolveDefaultReasoningEffort(modelItem);
|
|
4532
4596
|
if (!supported.length || !selectedEffort) {
|
|
4533
4597
|
// No effort step for this model — fall through to the context-tier
|
|
4534
4598
|
// step, which handles both the new-session and switch-model flows.
|
|
4535
|
-
await this.openContextTierPicker(modelItem, sessionOptions);
|
|
4599
|
+
await this.openContextTierPicker(modelItem, sessionOptions, previousFocus);
|
|
4536
4600
|
return;
|
|
4537
4601
|
}
|
|
4538
4602
|
|
|
@@ -4552,7 +4616,7 @@ export class PilotSwarmUiController {
|
|
|
4552
4616
|
: `Reasoning effort for ${modelItem?.modelName || modelItem?.qualifiedName || "model"}`,
|
|
4553
4617
|
items,
|
|
4554
4618
|
selectedIndex,
|
|
4555
|
-
previousFocus
|
|
4619
|
+
previousFocus,
|
|
4556
4620
|
modelItem,
|
|
4557
4621
|
sessionOptions,
|
|
4558
4622
|
},
|
|
@@ -4560,21 +4624,26 @@ export class PilotSwarmUiController {
|
|
|
4560
4624
|
this.dispatch({ type: "ui/status", text: "Select a reasoning effort and press Enter" });
|
|
4561
4625
|
}
|
|
4562
4626
|
|
|
4563
|
-
async openContextTierPicker(modelItem, sessionOptions = {}) {
|
|
4627
|
+
async openContextTierPicker(modelItem, sessionOptions = {}, previousFocusOverride = null) {
|
|
4564
4628
|
// Context-window tier step of the new-session flow. Only models whose
|
|
4565
4629
|
// catalog entry declares supportedContextTiers get this picker; all
|
|
4566
4630
|
// others skip straight to the agent picker. The preselected tier is
|
|
4567
4631
|
// the catalog default ("default", the smaller window).
|
|
4632
|
+
const previousFocus = previousFocusOverride ?? this.getState().ui.focusRegion;
|
|
4568
4633
|
const supported = normalizeContextTiers(modelItem?.supportedContextTiers);
|
|
4569
4634
|
const selectedTier = sessionOptions?.contextTier || resolveDefaultContextTier(modelItem);
|
|
4570
4635
|
if (!supported.length || !selectedTier) {
|
|
4571
4636
|
// Model declares no context-window tiers — skip straight to applying
|
|
4572
4637
|
// the switch (switch flow) or to the agent picker (new-session flow).
|
|
4573
4638
|
if (sessionOptions?.mode === "switchModel") {
|
|
4639
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
4640
|
+
if (previousFocus) {
|
|
4641
|
+
this.setFocus(previousFocus);
|
|
4642
|
+
}
|
|
4574
4643
|
await this.switchSessionModel({ ...sessionOptions, model: modelItem?.id });
|
|
4575
4644
|
return;
|
|
4576
4645
|
}
|
|
4577
|
-
await this.openSessionAgentPicker(sessionOptions);
|
|
4646
|
+
await this.openSessionAgentPicker(sessionOptions, previousFocus);
|
|
4578
4647
|
return;
|
|
4579
4648
|
}
|
|
4580
4649
|
|
|
@@ -4593,7 +4662,7 @@ export class PilotSwarmUiController {
|
|
|
4593
4662
|
title: `Context window for ${modelItem?.modelName || modelItem?.qualifiedName || "model"}`,
|
|
4594
4663
|
items,
|
|
4595
4664
|
selectedIndex,
|
|
4596
|
-
previousFocus
|
|
4665
|
+
previousFocus,
|
|
4597
4666
|
modelItem,
|
|
4598
4667
|
sessionOptions,
|
|
4599
4668
|
},
|
|
@@ -5914,17 +5983,20 @@ export class PilotSwarmUiController {
|
|
|
5914
5983
|
});
|
|
5915
5984
|
return;
|
|
5916
5985
|
}
|
|
5986
|
+
// Each successor opener replaces this modal itself, so the overlay
|
|
5987
|
+
// is NOT dismissed here: doing that before an awaited step blinked
|
|
5988
|
+
// the dialog off screen for the length of the step's fetch.
|
|
5917
5989
|
const previousFocus = modal.previousFocus;
|
|
5918
|
-
this.dispatch({ type: "ui/modal", modal: null });
|
|
5919
|
-
if (previousFocus) {
|
|
5920
|
-
this.setFocus(previousFocus);
|
|
5921
|
-
}
|
|
5922
5990
|
if (!item) {
|
|
5923
5991
|
if (modal.sessionOptions?.mode === "switchModel") {
|
|
5992
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
5993
|
+
if (previousFocus) {
|
|
5994
|
+
this.setFocus(previousFocus);
|
|
5995
|
+
}
|
|
5924
5996
|
this.dispatch({ type: "ui/status", text: "No model selected" });
|
|
5925
5997
|
return;
|
|
5926
5998
|
}
|
|
5927
|
-
await this.openSessionAgentPicker({});
|
|
5999
|
+
await this.openSessionAgentPicker({}, previousFocus);
|
|
5928
6000
|
return;
|
|
5929
6001
|
}
|
|
5930
6002
|
const defaultReasoning = resolveDefaultReasoningEffort(item);
|
|
@@ -5933,49 +6005,49 @@ export class PilotSwarmUiController {
|
|
|
5933
6005
|
...(modal.sessionOptions || {}),
|
|
5934
6006
|
model: item.id,
|
|
5935
6007
|
reasoningEffort: defaultReasoning ?? null,
|
|
5936
|
-
});
|
|
6008
|
+
}, previousFocus);
|
|
5937
6009
|
return;
|
|
5938
6010
|
}
|
|
5939
6011
|
await this.openReasoningEffortPicker(item, {
|
|
5940
6012
|
...(modal.sessionOptions || {}),
|
|
5941
6013
|
model: item.id,
|
|
5942
6014
|
...(defaultReasoning ? { reasoningEffort: defaultReasoning } : {}),
|
|
5943
|
-
});
|
|
6015
|
+
}, previousFocus);
|
|
5944
6016
|
return;
|
|
5945
6017
|
}
|
|
5946
6018
|
if (modal.type === "reasoningEffortPicker") {
|
|
5947
6019
|
const item = modal.items?.[modal.selectedIndex || 0];
|
|
5948
6020
|
const previousFocus = modal.previousFocus;
|
|
5949
6021
|
const sessionOptions = modal.sessionOptions || {};
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
this.setFocus(previousFocus);
|
|
5953
|
-
}
|
|
6022
|
+
// Not dismissed here — the tier step (or the agent step it falls
|
|
6023
|
+
// through to) replaces this modal in one dispatch.
|
|
5954
6024
|
await this.openContextTierPicker(modal.modelItem, {
|
|
5955
6025
|
...sessionOptions,
|
|
5956
6026
|
...(item?.id ? { reasoningEffort: item.id } : {}),
|
|
5957
|
-
});
|
|
6027
|
+
}, previousFocus);
|
|
5958
6028
|
return;
|
|
5959
6029
|
}
|
|
5960
6030
|
if (modal.type === "contextTierPicker") {
|
|
5961
6031
|
const item = modal.items?.[modal.selectedIndex || 0];
|
|
5962
6032
|
const previousFocus = modal.previousFocus;
|
|
5963
6033
|
const sessionOptions = modal.sessionOptions || {};
|
|
5964
|
-
this.dispatch({ type: "ui/modal", modal: null });
|
|
5965
|
-
if (previousFocus) {
|
|
5966
|
-
this.setFocus(previousFocus);
|
|
5967
|
-
}
|
|
5968
6034
|
const nextOptions = {
|
|
5969
6035
|
...sessionOptions,
|
|
5970
6036
|
...(item?.id ? { contextTier: item.id } : {}),
|
|
5971
6037
|
};
|
|
5972
|
-
// Switch-model flow ends here — apply the model/effort/tier switch
|
|
5973
|
-
//
|
|
6038
|
+
// Switch-model flow ends here — apply the model/effort/tier switch,
|
|
6039
|
+
// so this one DOES close the overlay. The new-session flow continues
|
|
6040
|
+
// to the agent picker, which replaces the modal itself once its
|
|
6041
|
+
// agent list has loaded.
|
|
5974
6042
|
if (sessionOptions?.mode === "switchModel") {
|
|
6043
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
6044
|
+
if (previousFocus) {
|
|
6045
|
+
this.setFocus(previousFocus);
|
|
6046
|
+
}
|
|
5975
6047
|
await this.switchSessionModel({ ...nextOptions, model: modal.modelItem?.id || sessionOptions.model });
|
|
5976
6048
|
return;
|
|
5977
6049
|
}
|
|
5978
|
-
await this.openSessionAgentPicker(nextOptions);
|
|
6050
|
+
await this.openSessionAgentPicker(nextOptions, previousFocus);
|
|
5979
6051
|
return;
|
|
5980
6052
|
}
|
|
5981
6053
|
if (modal.type === "sessionAgentPicker") {
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -53,8 +53,38 @@ function isPinnableSession(session) {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Keep a pin unless the row is loaded AND demonstrably no longer pinnable.
|
|
58
|
+
*
|
|
59
|
+
* Same rule, and the same reasoning, as pruneCollapsedIds below: a row missing
|
|
60
|
+
* from the current listing has NOT been unpinned. Not-loaded, filtered out,
|
|
61
|
+
* on another page, or simply slower to arrive are indistinguishable from here,
|
|
62
|
+
* and none of them is the user changing their mind.
|
|
63
|
+
*
|
|
64
|
+
* This ran unguarded on every `sessions/loaded`, so any listing that did not
|
|
65
|
+
* happen to contain the pinned row dropped the pin — and because pinnedIds is
|
|
66
|
+
* a dependency of the profile-save effect, the emptied list was then written
|
|
67
|
+
* back to the server as the user's preference. That is a one-way ratchet: the
|
|
68
|
+
* pin did not survive a restart, and the destroyed preference propagated to
|
|
69
|
+
* every other device, which is why a pin set on the desktop never reached the
|
|
70
|
+
* phone.
|
|
71
|
+
*
|
|
72
|
+
* A session that is genuinely present and NOT pinnable (moved into a group or
|
|
73
|
+
* under a parent, or a system row) is a real signal, so that still drops.
|
|
74
|
+
*/
|
|
56
75
|
function prunePinnedIds(ids, byId) {
|
|
57
|
-
|
|
76
|
+
const out = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const id of ids || []) {
|
|
79
|
+
if (!id || seen.has(id)) continue;
|
|
80
|
+
seen.add(id);
|
|
81
|
+
const session = byId?.[id];
|
|
82
|
+
// Absent from this listing tells us nothing — keep it. A stale id
|
|
83
|
+
// matches no row and hoists nothing; a wrongly dropped one costs the
|
|
84
|
+
// user their pin permanently.
|
|
85
|
+
if (!session || isPinnableSession(session)) out.push(id);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
|
|
@@ -2128,6 +2158,10 @@ export function appReducer(state, action) {
|
|
|
2128
2158
|
detail: null,
|
|
2129
2159
|
detailLoading: Boolean(action.name),
|
|
2130
2160
|
detailError: null,
|
|
2161
|
+
// Cleared on every selection: showing the previous
|
|
2162
|
+
// package's history under a new package is worse than
|
|
2163
|
+
// showing none.
|
|
2164
|
+
changelog: null,
|
|
2131
2165
|
action: { pending: null, error: null },
|
|
2132
2166
|
workspace: {
|
|
2133
2167
|
tree: null, treeLoading: Boolean(action.name), treeError: null,
|
|
@@ -2146,6 +2180,21 @@ export function appReducer(state, action) {
|
|
|
2146
2180
|
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2147
2181
|
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, detail: null, detailLoading: false, detailError: action.error || "Failed to load package" } } };
|
|
2148
2182
|
}
|
|
2183
|
+
case "admin/packages/changelog/loaded": {
|
|
2184
|
+
// Stale-response guard, same as the tree: a slow changelog for a
|
|
2185
|
+
// previously-selected package must not surface under the current one.
|
|
2186
|
+
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2187
|
+
return {
|
|
2188
|
+
...state,
|
|
2189
|
+
admin: {
|
|
2190
|
+
...state.admin,
|
|
2191
|
+
packages: {
|
|
2192
|
+
...state.admin.packages,
|
|
2193
|
+
changelog: action.content || null,
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2149
2198
|
case "admin/packages/tree/loaded": {
|
|
2150
2199
|
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2151
2200
|
const topDirs = Array.isArray(action.tree?.dirs)
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -256,7 +256,7 @@ export function manualOrderContainerKey(session, pinned = false) {
|
|
|
256
256
|
* plus a createdBy email. Resolving the package's principal against this
|
|
257
257
|
* directory recovers the real name, which is what makes initials come out as
|
|
258
258
|
* a person's initials ("Affan Dar" → AD) rather than the first two letters of
|
|
259
|
-
* an alias ("
|
|
259
|
+
* an alias ("ada@…" → AD). No fetch: the data is already loaded.
|
|
260
260
|
*/
|
|
261
261
|
/**
|
|
262
262
|
* The one owner-badge descriptor both lists render from. Same person ⇒ same
|
|
@@ -290,7 +290,7 @@ function ownerDirectoryFromSessions(byId) {
|
|
|
290
290
|
*
|
|
291
291
|
* A DISPLAY NAME beats an email, because initials should be the person's
|
|
292
292
|
* ("Affan Dar" → AD), not the first two letters of their alias
|
|
293
|
-
* ("
|
|
293
|
+
* ("ada@…" → AD) — which is exactly the mismatch that made the same
|
|
294
294
|
* person read differently in the session list and the package list.
|
|
295
295
|
*/
|
|
296
296
|
function resolvePackageOwner(pkg, directory) {
|
|
@@ -342,29 +342,50 @@ export function ownerBadgeKey(owner, createdBy) {
|
|
|
342
342
|
).trim().toLowerCase();
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Distinct HUMAN owners **in the list** — where "the list" means the rows the
|
|
347
|
+
* owner filter admits, whether or not they happen to be on screen.
|
|
348
|
+
*
|
|
349
|
+
* That distinction is the whole rule, and the two halves pull in opposite
|
|
350
|
+
* directions on purpose:
|
|
351
|
+
* - **Collapsing** a folder hides rows that are still in the list, so they
|
|
352
|
+
* still count. A chip that appeared on expand and vanished on collapse
|
|
353
|
+
* would be worse than no chip.
|
|
354
|
+
* - **Filtering** takes rows out of the list entirely, so they stop counting.
|
|
355
|
+
* Narrowing to "me + System" leaves one human, and one human needs no chip.
|
|
356
|
+
*
|
|
357
|
+
* "Human" excludes:
|
|
358
|
+
* - `isSystem` rows (the permanent system agents);
|
|
359
|
+
* - rows owned by the first-class System principal. Sessions a system agent
|
|
360
|
+
* spawns on a user's behalf are NOT flagged isSystem — deliberately, so
|
|
361
|
+
* they stay deletable (see SYSTEM_OWNER_KEY) — so gating on the flag alone
|
|
362
|
+
* let "System" through as a second "person";
|
|
363
|
+
* - unowned rows, whose owner key is null.
|
|
364
|
+
*/
|
|
365
|
+
function distinctHumanOwnerCount(state, stopAt = Infinity) {
|
|
366
|
+
const byId = state?.sessions?.byId;
|
|
367
|
+
const ownerFilter = state?.sessions?.ownerFilter || { all: true };
|
|
368
|
+
const auth = state?.auth || {};
|
|
359
369
|
const owners = new Set();
|
|
360
|
-
for (const session of Object.values(
|
|
370
|
+
for (const session of Object.values(byId || {})) {
|
|
361
371
|
if (!session || session.isSystem || session.isGroup) continue;
|
|
362
372
|
const key = ownerKeyForOwner(session.owner);
|
|
363
|
-
if (!key) continue;
|
|
373
|
+
if (!key || key === SYSTEM_OWNER_KEY) continue;
|
|
374
|
+
// Only rows the filter admits. Uses the same predicate the rows
|
|
375
|
+
// themselves are filtered with, so the tally can never disagree with
|
|
376
|
+
// what is actually listed.
|
|
377
|
+
if (!matchesOwnerFilterDirect(session, ownerFilter, auth)) continue;
|
|
364
378
|
owners.add(key);
|
|
365
|
-
if (owners.size
|
|
379
|
+
if (owners.size >= stopAt) break;
|
|
366
380
|
}
|
|
367
|
-
return
|
|
381
|
+
return owners.size;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function shouldDecorateSessionOwners(state) {
|
|
385
|
+
// One rule: the chip earns its place when the list holds more than one
|
|
386
|
+
// distinct human owner. Otherwise it is noise on every row — you are
|
|
387
|
+
// always "you".
|
|
388
|
+
return distinctHumanOwnerCount(state, 2) > 1;
|
|
368
389
|
}
|
|
369
390
|
|
|
370
391
|
function groupMemberSessions(group, byId = {}) {
|
|
@@ -3802,6 +3823,18 @@ export function selectAdminConsole(state) {
|
|
|
3802
3823
|
);
|
|
3803
3824
|
// Built once per render, not per row.
|
|
3804
3825
|
const ownerDirectory = ownerDirectoryFromSessions(state?.sessions?.byId);
|
|
3826
|
+
// Same rule as the session list: the owner chip only earns its place when
|
|
3827
|
+
// this list actually holds more than one owner. Shared packages belong to
|
|
3828
|
+
// the deployment, not a person, so they neither carry a chip nor count
|
|
3829
|
+
// toward the tally.
|
|
3830
|
+
const distinctPackageOwners = new Set();
|
|
3831
|
+
for (const pkg of pkgList) {
|
|
3832
|
+
if (pkg?.scope === "shared") continue;
|
|
3833
|
+
const key = ownerKeyForOwner(pkg?.owner) || String(pkg?.createdBy || "").trim().toLowerCase();
|
|
3834
|
+
if (!key || key === SYSTEM_OWNER_KEY) continue;
|
|
3835
|
+
distinctPackageOwners.add(key);
|
|
3836
|
+
}
|
|
3837
|
+
const decoratePackageOwners = distinctPackageOwners.size > 1;
|
|
3805
3838
|
const packageRow = (pkg) => ({
|
|
3806
3839
|
name: pkg.name,
|
|
3807
3840
|
scope: pkg.scope === "shared" ? "shared" : "user",
|
|
@@ -3815,7 +3848,9 @@ export function selectAdminConsole(state) {
|
|
|
3815
3848
|
// the deployment and keep the scope badge instead.
|
|
3816
3849
|
// Resolved against the session-owner directory so the initials are the
|
|
3817
3850
|
// PERSON's, matching the session list exactly.
|
|
3818
|
-
ownerBadge: pkg.scope === "shared"
|
|
3851
|
+
ownerBadge: (pkg.scope === "shared" || !decoratePackageOwners)
|
|
3852
|
+
? null
|
|
3853
|
+
: ownerBadgeFor(resolvePackageOwner(pkg, ownerDirectory)),
|
|
3819
3854
|
// Highlight only while the Agents section is active — otherwise the
|
|
3820
3855
|
// GitHub Keys screen shows a double selection.
|
|
3821
3856
|
selected: admin.section === "packages" && pkgState.selectedName === pkg.name,
|
|
@@ -4074,6 +4109,14 @@ export function selectAdminConsole(state) {
|
|
|
4074
4109
|
selectedName: pkgState.selectedName || null,
|
|
4075
4110
|
detail: packageDetail,
|
|
4076
4111
|
workspace,
|
|
4112
|
+
/**
|
|
4113
|
+
* The package's own CHANGELOG.md, when it ships one.
|
|
4114
|
+
*
|
|
4115
|
+
* Passed through verbatim rather than parsed: it is authored markdown,
|
|
4116
|
+
* and the renderer only needs to pick out headings and the signature
|
|
4117
|
+
* line. Null when the package carries no changelog.
|
|
4118
|
+
*/
|
|
4119
|
+
changelog: pkgState.changelog || null,
|
|
4077
4120
|
addDialog: {
|
|
4078
4121
|
open: Boolean(pkgState.addDialog?.open),
|
|
4079
4122
|
kind: pkgState.addDialog?.kind || "repo",
|
|
@@ -7255,7 +7298,10 @@ export function selectInspector(state, options = {}) {
|
|
|
7255
7298
|
]
|
|
7256
7299
|
: activeTab === "history"
|
|
7257
7300
|
? [
|
|
7258
|
-
|
|
7301
|
+
// Named for what it actually shows: the durable Duroxide
|
|
7302
|
+
// event log. The compact branch abbreviates rather than
|
|
7303
|
+
// truncates — the full name overruns a narrow inspector.
|
|
7304
|
+
...buildPaneTitleRuns(compactSecondaryMeta ? "Duroxide Events" : `Duroxide Event History: ${shortId}`, "magenta"),
|
|
7259
7305
|
]
|
|
7260
7306
|
: activeTab === "stats"
|
|
7261
7307
|
? [
|
package/ui/core/src/state.js
CHANGED
|
@@ -343,6 +343,8 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
343
343
|
detail: null,
|
|
344
344
|
detailLoading: false,
|
|
345
345
|
detailError: null,
|
|
346
|
+
/** Raw CHANGELOG.md of the selected package, or null when it has none. */
|
|
347
|
+
changelog: null,
|
|
346
348
|
workspace: {
|
|
347
349
|
tree: null,
|
|
348
350
|
treeLoading: false,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createTheme } from "./helpers.js";
|
|
2
|
+
|
|
3
|
+
// DOOM (id Software, 1993) — the status bar, not the shooter.
|
|
4
|
+
//
|
|
5
|
+
// The recognizable thing about DOOM's UI is STBAR: a band of hammered brown
|
|
6
|
+
// metal carrying big red numerals and the marine's face. That maps onto a
|
|
7
|
+
// portal header almost directly, so this theme is warm brown chrome around a
|
|
8
|
+
// near-black ground, with bone-coloured type.
|
|
9
|
+
//
|
|
10
|
+
// The semantic palette is the THREE KEYCARDS — blue, yellow, red. Every player
|
|
11
|
+
// already reads that triad as progress / caution / stop without being taught,
|
|
12
|
+
// and it happens to be exactly the distinction a session list needs. Armor
|
|
13
|
+
// green is held back for "completed".
|
|
14
|
+
//
|
|
15
|
+
// The ground is a red-biased near-black rather than pure #000: DOOM's letterbox
|
|
16
|
+
// was black but everything in frame was warm, and a neutral black underneath
|
|
17
|
+
// warm chrome reads as two unrelated themes stacked.
|
|
18
|
+
const doomTheme = createTheme({
|
|
19
|
+
id: "doom",
|
|
20
|
+
label: "DOOM",
|
|
21
|
+
description: "STBAR brown metal, bone type, and the three keycards as the status palette.",
|
|
22
|
+
// The face, drawn as data rather than markup so ui/core stays free of any
|
|
23
|
+
// framework and nothing has to inject raw HTML. The portal renders it in
|
|
24
|
+
// place of the deployment's logo; the TUI ignores it.
|
|
25
|
+
//
|
|
26
|
+
// In DOOM the face was never decoration — it reported your state. Keeping
|
|
27
|
+
// it as the mark leaves that door open.
|
|
28
|
+
icon: {
|
|
29
|
+
viewBox: "0 0 32 32",
|
|
30
|
+
paths: [
|
|
31
|
+
{ d: "M0 0h32v32H0z", fill: "#2a1d14" }, // recessed frame
|
|
32
|
+
{ d: "M6 7h20v20H6z", fill: "#c89a6a" }, // face
|
|
33
|
+
{ d: "M5 6h22v6H5z", fill: "#4a2f1a" }, // hair
|
|
34
|
+
{ d: "M9 15h5v3.4H9z", fill: "#1a1008" }, // left eye
|
|
35
|
+
{ d: "M18 15h5v3.4h-5z", fill: "#1a1008" }, // right eye
|
|
36
|
+
{ d: "M8.6 13.6l6 1 -.3 1.7-6-1z", fill: "#3a2412" }, // brows, angled in
|
|
37
|
+
{ d: "M23.4 13.6l-6 1 .3 1.7 6-1z", fill: "#3a2412" },
|
|
38
|
+
{ d: "M14.5 18.5h3v2.4h-3z", fill: "#a8734a" }, // nose
|
|
39
|
+
{ d: "M11 22h10v1.8H11z", fill: "#7a3a2a" }, // mouth
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
page: {
|
|
43
|
+
background: "#100b09",
|
|
44
|
+
foreground: "#d9cba8",
|
|
45
|
+
overlayBackground: "#1c1310",
|
|
46
|
+
overlayForeground: "#d9cba8",
|
|
47
|
+
// 6.2:1 on the ground. Authentic DOOM shadow browns sit nearer 3:1 and
|
|
48
|
+
// would fail the muted-contrast bar the theme suite enforces, so this
|
|
49
|
+
// is lifted — the same deliberate deviation ms-dos.js documents for CGA
|
|
50
|
+
// dark grey.
|
|
51
|
+
hintColor: "#a08d68",
|
|
52
|
+
modalBackdrop: "rgba(16, 11, 9, 0.82)",
|
|
53
|
+
modalBackground: "#1c1310",
|
|
54
|
+
modalBorder: "#6f5432",
|
|
55
|
+
modalForeground: "#d9cba8",
|
|
56
|
+
modalMuted: "#a08d68",
|
|
57
|
+
// Selection is the STBAR band itself, not a tint.
|
|
58
|
+
modalSelectedBackground: "#6f5432",
|
|
59
|
+
modalSelectedBorder: "#a4885c",
|
|
60
|
+
modalSelectedForeground: "#fff4d6",
|
|
61
|
+
},
|
|
62
|
+
terminal: {
|
|
63
|
+
background: "#100b09",
|
|
64
|
+
foreground: "#d9cba8",
|
|
65
|
+
cursor: "#b81c1c",
|
|
66
|
+
cursorAccent: "#100b09",
|
|
67
|
+
selectionBackground: "#6f5432",
|
|
68
|
+
black: "#100b09",
|
|
69
|
+
// Health red, lifted a shade: the sprite value lands at 2.9994:1 on
|
|
70
|
+
// this ground — close enough to look fine and just under the 3:1 a
|
|
71
|
+
// status mark needs.
|
|
72
|
+
red: "#c42222",
|
|
73
|
+
green: "#3f9e3f", // armor bonus
|
|
74
|
+
yellow: "#e8c020", // yellow keycard
|
|
75
|
+
// Lifted from the sprite's #3050d0, which measures 2.97:1 on this
|
|
76
|
+
// ground — under the 3:1 a status dot needs. Still unmistakably the
|
|
77
|
+
// blue keycard, now legible.
|
|
78
|
+
blue: "#3a5ce0",
|
|
79
|
+
magenta: "#a0446a",
|
|
80
|
+
cyan: "#4a8f8f",
|
|
81
|
+
white: "#d9cba8", // bone
|
|
82
|
+
brightBlack: "#a08d68",
|
|
83
|
+
brightRed: "#ff3a1e",
|
|
84
|
+
brightGreen: "#5fc45f",
|
|
85
|
+
brightYellow: "#ffd84a",
|
|
86
|
+
brightBlue: "#5a7ae8",
|
|
87
|
+
brightMagenta: "#c46a94",
|
|
88
|
+
brightCyan: "#6fb5b5",
|
|
89
|
+
brightWhite: "#fff4d6",
|
|
90
|
+
},
|
|
91
|
+
tui: {
|
|
92
|
+
border: "#6f5432",
|
|
93
|
+
// The transcript is prose, so speakers keep the keycard colours rather
|
|
94
|
+
// than everything collapsing into bone-on-brown.
|
|
95
|
+
userChat: "#e8c020",
|
|
96
|
+
userChatLabel: "#ffd84a",
|
|
97
|
+
selectionBackground: "#6f5432",
|
|
98
|
+
selectionForeground: "#fff4d6",
|
|
99
|
+
activeHighlightBackground: "#6f5432",
|
|
100
|
+
activeHighlightForeground: "#fff4d6",
|
|
101
|
+
promptCursorBackground: "#b81c1c",
|
|
102
|
+
promptCursorForeground: "#fff4d6",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export default doomTheme;
|
|
@@ -41,7 +41,7 @@ export function isThemeLight(theme) {
|
|
|
41
41
|
return getRelativeLuminance(background) >= 0.5;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function createTheme({ id, label, description, page, terminal, tui = {}, richChat = false }) {
|
|
44
|
+
export function createTheme({ id, label, description, page, terminal, tui = {}, richChat = false, icon = null }) {
|
|
45
45
|
const baseTui = {
|
|
46
46
|
background: page?.background || terminal?.background || "#000000",
|
|
47
47
|
surface: terminal?.background || page?.background || "#000000",
|
|
@@ -76,6 +76,12 @@ export function createTheme({ id, label, description, page, terminal, tui = {},
|
|
|
76
76
|
// Portal-only: this theme renders the chat transcript in the rich
|
|
77
77
|
// (desktop-style) view. The TUI ignores it.
|
|
78
78
|
richChat: Boolean(richChat),
|
|
79
|
+
// Portal-only: an optional brand mark carried BY the theme. When set it
|
|
80
|
+
// outranks the deployment's own logo, because a theme that restyles the
|
|
81
|
+
// whole shell and then leaves a foreign logo in the corner reads as
|
|
82
|
+
// broken. Data, not markup — ui/core has no framework and must not
|
|
83
|
+
// inject HTML. Shape: { viewBox, paths: [{ d, fill }] }.
|
|
84
|
+
icon: icon || null,
|
|
79
85
|
page,
|
|
80
86
|
terminal,
|
|
81
87
|
tui: {
|
|
@@ -15,6 +15,8 @@ import terminalGreenTheme from "./terminal-green.js";
|
|
|
15
15
|
import highContrastMonoTheme from "./high-contrast-mono.js";
|
|
16
16
|
import win95Theme from "./win95.js";
|
|
17
17
|
import msDosTheme from "./ms-dos.js";
|
|
18
|
+
import doomTheme from "./doom.js";
|
|
19
|
+
import winampTheme from "./winamp.js";
|
|
18
20
|
|
|
19
21
|
const THEMES = Object.freeze([
|
|
20
22
|
workspaceDarkTheme,
|
|
@@ -34,6 +36,8 @@ const THEMES = Object.freeze([
|
|
|
34
36
|
highContrastMonoTheme,
|
|
35
37
|
win95Theme,
|
|
36
38
|
msDosTheme,
|
|
39
|
+
doomTheme,
|
|
40
|
+
winampTheme,
|
|
37
41
|
].sort((left, right) => left.label.localeCompare(right.label, undefined, { sensitivity: "base" })));
|
|
38
42
|
|
|
39
43
|
const THEME_MAP = new Map(THEMES.map((theme) => [theme.id, theme]));
|