@@ -80,6 +80,38 @@ export const PlannerTab = {
`;
+ const profileSelect = container.querySelector("#planner-profile");
+ const fallbackProfiles = [
+ { id: "SDXL-v1", label: "SDXL v1" },
+ { id: "Flux-Dev", label: "Flux Dev" },
+ ];
+ const applyProfiles = (profiles, defaultProfile = "SDXL-v1") => {
+ const current = profileSelect.value;
+ profileSelect.innerHTML = "";
+ (profiles || []).forEach((profile) => {
+ const option = document.createElement("option");
+ option.value = profile.id;
+ option.textContent = profile.label || profile.id;
+ profileSelect.appendChild(option);
+ });
+ const desired = current && [...profileSelect.options].some((opt) => opt.value === current)
+ ? current
+ : defaultProfile;
+ if (desired) {
+ profileSelect.value = desired;
+ }
+ };
+ try {
+ const profileRes = await openclawApi.listPlannerProfiles();
+ if (profileRes.ok && Array.isArray(profileRes.data?.profiles) && profileRes.data.profiles.length > 0) {
+ applyProfiles(profileRes.data.profiles, profileRes.data.default_profile || "SDXL-v1");
+ } else {
+ applyProfiles(fallbackProfiles, "SDXL-v1");
+ }
+ } catch {
+ applyProfiles(fallbackProfiles, "SDXL-v1");
+ }
+
const lifecycle = createRequestLifecycleController(container, {
loading: "#planner-loading",
runButton: "#planner-run-btn",
diff --git a/web/tests/unit/planner_tab.test.js b/web/tests/unit/planner_tab.test.js
new file mode 100644
index 0000000..5b7c9f7
--- /dev/null
+++ b/web/tests/unit/planner_tab.test.js
@@ -0,0 +1,73 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { apiMock } = vi.hoisted(() => ({
+ apiMock: {
+ listPlannerProfiles: vi.fn(),
+ },
+}));
+
+vi.mock("../../openclaw_api.js", () => ({
+ openclawApi: apiMock,
+}));
+
+vi.mock("../../openclaw_utils.js", () => ({
+ showError: vi.fn(),
+ clearError: vi.fn(),
+ showToast: vi.fn(),
+ createRequestLifecycleController: vi.fn(() => ({
+ begin: vi.fn(() => null),
+ setStage: vi.fn(),
+ end: vi.fn(),
+ cancel: vi.fn(() => false),
+ })),
+}));
+
+import { PlannerTab } from "../../tabs/planner_tab.js";
+
+describe("planner_tab", () => {
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ apiMock.listPlannerProfiles.mockReset();
+ });
+
+ it("loads planner profiles from the backend registry", async () => {
+ apiMock.listPlannerProfiles.mockResolvedValue({
+ ok: true,
+ data: {
+ profiles: [
+ { id: "Photo", label: "Photo Real" },
+ { id: "Sketch", label: "Sketch Draft" },
+ ],
+ default_profile: "Sketch",
+ },
+ });
+
+ const container = document.createElement("div");
+ await PlannerTab.render(container);
+
+ const select = container.querySelector("#planner-profile");
+ const options = [...select.querySelectorAll("option")].map((opt) => ({
+ value: opt.value,
+ text: opt.textContent,
+ }));
+
+ expect(options).toEqual([
+ { value: "Photo", text: "Photo Real" },
+ { value: "Sketch", text: "Sketch Draft" },
+ ]);
+ expect(select.value).toBe("Sketch");
+ });
+
+ it("falls back to built-in profiles when the API fails", async () => {
+ apiMock.listPlannerProfiles.mockRejectedValue(new Error("network down"));
+
+ const container = document.createElement("div");
+ await PlannerTab.render(container);
+
+ const select = container.querySelector("#planner-profile");
+ const options = [...select.querySelectorAll("option")].map((opt) => opt.value);
+
+ expect(options).toEqual(["SDXL-v1", "Flux-Dev"]);
+ expect(select.value).toBe("SDXL-v1");
+ });
+});