Add office agent management wizard (#56)

* Add agents

* Agent

* Added agents management

* Polish agent wizard and release blockers.

Finalize the office agent management flow by aligning the gateway fallback behavior, cleaning up UI semantics, and updating tests so the branch is ready to ship.

Made-with: Cursor

---------

Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
This commit is contained in:
Luke The Dev
2026-03-23 18:04:37 -05:00
committed by GitHub
parent 5e7812c352
commit c9789c2148
32 changed files with 1504 additions and 181 deletions
+15 -13
View File
@@ -111,14 +111,14 @@ describe("AgentBrainPanel", () => {
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Persona" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "SOUL.md" })).toBeInTheDocument();
});
expect(screen.getByRole("heading", { name: "Directives" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Context" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Identity" })).toBeInTheDocument();
expect(screen.getByLabelText("Directives")).toHaveValue("alpha agents");
expect(screen.getByLabelText("Persona")).toHaveValue(
expect(screen.getByRole("heading", { name: "AGENTS.md" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "USER.md" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "IDENTITY.md" })).toBeInTheDocument();
expect(screen.getByLabelText("AGENTS.md")).toHaveValue("alpha agents");
expect(screen.getByLabelText("SOUL.md")).toHaveValue(
"# SOUL.md - Who You Are\n\n## Core Truths\n\nBe useful."
);
expect(screen.getByLabelText("Name")).toHaveValue("Alpha");
@@ -154,10 +154,10 @@ describe("AgentBrainPanel", () => {
);
await waitFor(() => {
expect(screen.getByLabelText("Directives")).toBeInTheDocument();
expect(screen.getByLabelText("AGENTS.md")).toBeInTheDocument();
});
fireEvent.change(screen.getByLabelText("Directives"), {
fireEvent.change(screen.getByLabelText("AGENTS.md"), {
target: { value: "alpha directives updated" },
});
@@ -171,15 +171,17 @@ describe("AgentBrainPanel", () => {
expect(filesByAgent["agent-1"]["AGENTS.md"]).toBe("alpha directives updated");
});
it("discards_unsaved_changes_without_writing_files", async () => {
it("calls_cancel_without_writing_files", async () => {
const { client, calls } = createMockClient();
const agents = [createAgent("agent-1", "Alpha", "session-1")];
const onCancel = vi.fn();
render(
createElement(AgentBrainPanel, {
client,
agents,
selectedAgentId: "agent-1",
onCancel,
})
);
@@ -192,12 +194,12 @@ describe("AgentBrainPanel", () => {
});
expect(screen.getByLabelText("Name")).toHaveValue("Alpha Prime");
fireEvent.click(screen.getByRole("button", { name: "Discard" }));
expect(screen.getByLabelText("Name")).toHaveValue("Alpha");
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
expect(calls.some((entry) => entry.method === "agents.files.set")).toBe(false);
});
it("does_not_render_name_editor_in_personality_panel", async () => {
it("does_not_render_legacy_name_editor_controls", async () => {
const { client } = createMockClient();
const agents = [createAgent("agent-1", "Alpha", "session-1")];
@@ -210,7 +212,7 @@ describe("AgentBrainPanel", () => {
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Persona" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "SOUL.md" })).toBeInTheDocument();
});
expect(screen.queryByLabelText("Agent name")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Update Name" })).not.toBeInTheDocument();
+1 -1
View File
@@ -615,7 +615,7 @@ describe("AgentSettingsPanel", () => {
target: { value: "git" },
});
fireEvent.click(screen.getByRole("button", { name: "Configure" }));
fireEvent.click(screen.getByRole("button", { name: "Remove skill from gateway" }));
fireEvent.click(screen.getByRole("button", { name: "Remove for all agents" }));
expect(onRemoveSkill).toHaveBeenCalledWith({
skillKey: "github",
+2 -3
View File
@@ -18,7 +18,7 @@ describe("resolveGatewayAutoRetryDelayMs", () => {
expect(delay).toBeNull();
});
it("retries for non-auth connect failures", () => {
it("does not retry when the upstream websocket upgrade fails", () => {
const delay = resolveGatewayAutoRetryDelayMs({
status: "disconnected",
didAutoConnect: true,
@@ -31,8 +31,7 @@ describe("resolveGatewayAutoRetryDelayMs", () => {
attempt: 0,
});
expect(delay).toBeTypeOf("number");
expect(delay).toBeGreaterThan(0);
expect(delay).toBeNull();
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { useOnboardingState } from "@/features/onboarding/useOnboardingState";
+2 -2
View File
@@ -68,7 +68,7 @@ describe("settingsRouteWorkflow", () => {
})
).toEqual([
{ kind: "set-personality-dirty", value: false },
{ kind: "push", href: "/agents" },
{ kind: "push", href: "/" },
]);
});
@@ -169,7 +169,7 @@ describe("settingsRouteWorkflow", () => {
hasRouteAgent: false,
currentInspectSidebar: null,
})
).toEqual([{ kind: "replace", href: "/agents" }]);
).toEqual([{ kind: "replace", href: "/" }]);
});
it("plans non-route selection reconciliation", () => {
@@ -8,7 +8,7 @@ import type { CronRunResult } from "@/lib/cron/types";
import type { MutationBlockState } from "@/features/agents/operations/mutationLifecycleWorkflow";
import { useAgentSettingsMutationController } from "@/features/agents/operations/useAgentSettingsMutationController";
import { deleteAgentViaStudio } from "@/features/agents/operations/deleteAgentOperation";
import { deleteAgentRecordViaStudio } from "@/features/agents/operations/deleteAgentOperation";
import { performCronCreateFlow } from "@/features/agents/operations/cronCreateOperation";
import { updateAgentPermissionsViaStudio } from "@/features/agents/operations/agentPermissionsOperation";
import { runAgentConfigMutationLifecycle } from "@/features/agents/operations/mutationLifecycleWorkflow";
@@ -50,7 +50,7 @@ vi.mock("@/features/agents/operations/useGatewayRestartBlock", () => ({
}));
vi.mock("@/features/agents/operations/deleteAgentOperation", () => ({
deleteAgentViaStudio: vi.fn(),
deleteAgentRecordViaStudio: vi.fn(),
}));
vi.mock("@/features/agents/operations/cronCreateOperation", () => ({
@@ -219,7 +219,7 @@ const renderController = (overrides?: Partial<Parameters<typeof useAgentSettings
};
describe("useAgentSettingsMutationController", () => {
const mockedDeleteAgentViaStudio = vi.mocked(deleteAgentViaStudio);
const mockedDeleteAgentViaStudio = vi.mocked(deleteAgentRecordViaStudio);
const mockedPerformCronCreateFlow = vi.mocked(performCronCreateFlow);
const mockedRunCronJobNow = vi.mocked(runCronJobNow);
const mockedRemoveCronJob = vi.mocked(removeCronJob);
@@ -347,7 +347,7 @@ describe("useAgentSettingsMutationController", () => {
deps.clearBlock();
return true;
});
mockedDeleteAgentViaStudio.mockResolvedValue({ trashed: { trashDir: "", moved: [] }, restored: null });
mockedDeleteAgentViaStudio.mockResolvedValue(undefined);
const ctx = renderController();
@@ -653,13 +653,15 @@ describe("useAgentSettingsMutationController", () => {
});
});
expect(mockedRemoveSkillFromGateway).toHaveBeenCalledWith({
skillKey: "browser",
source: "openclaw-workspace",
baseDir: "/tmp/workspace/skills/browser",
workspaceDir: "/tmp/workspace",
managedSkillsDir: "/tmp/skills",
});
expect(mockedRemoveSkillFromGateway).toHaveBeenCalledWith(
expect.objectContaining({
skillKey: "browser",
source: "openclaw-workspace",
baseDir: "/tmp/workspace/skills/browser",
workspaceDir: "/tmp/workspace",
managedSkillsDir: "/tmp/skills",
})
);
expect(ctx.enqueueConfigMutation).toHaveBeenCalledWith(
expect.objectContaining({ kind: "update-skill-setup" })
);
@@ -276,7 +276,7 @@ describe("useSettingsRouteController", () => {
});
await waitFor(() => {
expect(ctx.replace).toHaveBeenCalledWith("/agents");
expect(ctx.replace).toHaveBeenCalledWith("/");
});
});