Avatar Customization + Update Agent Brain (#23)

Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Luke The Dev
2026-03-20 23:05:14 -05:00
committed by GitHub
parent a5b0895dd8
commit 65c2b9cf85
39 changed files with 2803 additions and 551 deletions
@@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { AgentAvatarCreatorModal } from "@/features/agents/components/AgentAvatarCreatorModal";
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
vi.mock("@/features/agents/components/AgentAvatarPreview3D", () => ({
AgentAvatarPreview3D: () => <div data-testid="avatar-preview-3d">preview</div>,
}));
describe("AgentAvatarCreatorModal", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("saves the edited avatar profile", async () => {
const initialProfile = createDefaultAgentAvatarProfile("seed-a");
const onSave = vi.fn(async () => {});
render(
<AgentAvatarCreatorModal
open
agentId="agent-1"
agentName="Agent One"
initialProfile={initialProfile}
onClose={() => {}}
onSave={onSave}
/>
);
fireEvent.click(screen.getByRole("button", { name: "Backpack" }));
fireEvent.click(screen.getByRole("button", { name: "Save avatar" }));
expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledWith(
expect.objectContaining({
seed: "seed-a",
accessories: expect.objectContaining({
backpack: !initialProfile.accessories.backpack,
}),
})
);
});
});
+137
View File
@@ -0,0 +1,137 @@
import { createElement } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { AgentEditorModal } from "@/features/agents/components/AgentEditorModal";
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
import type { AgentState } from "@/features/agents/state/store";
import type { GatewayClient } from "@/lib/gateway/GatewayClient";
vi.mock("@/features/agents/components/AgentAvatarPreview3D", () => ({
AgentAvatarPreview3D: () => createElement("div", { "data-testid": "avatar-preview-3d" }, "preview"),
}));
vi.mock("@/features/agents/components/inspect/AgentBrainPanel", () => ({
AgentBrainPanel: ({
selectedAgentId,
activeSection,
}: {
selectedAgentId: string | null;
activeSection?: string;
}) =>
createElement(
"div",
{ "data-testid": "brain-panel" },
`brain:${selectedAgentId}:${activeSection ?? "all"}`,
),
}));
const buildAgent = (): AgentState =>
({
agentId: "agent-1",
name: "Agent One",
avatarProfile: createDefaultAgentAvatarProfile("seed-a"),
avatarSeed: "seed-a",
avatarUrl: null,
status: "idle",
sessionCreated: false,
awaitingUserInput: false,
hasUnseenActivity: false,
outputLines: [],
lastResult: null,
lastDiff: null,
runId: null,
runStartedAt: null,
streamText: null,
thinkingTrace: null,
latestOverride: null,
latestOverrideKind: null,
lastAssistantMessageAt: null,
lastActivityAt: null,
latestPreview: null,
lastUserMessage: null,
draft: "",
queuedMessages: [],
sessionSettingsSynced: false,
historyLoadedAt: null,
historyFetchLimit: null,
historyFetchedCount: null,
historyMaybeTruncated: false,
toolCallingEnabled: true,
showThinkingTraces: false,
sessionKey: "session-1",
model: undefined,
thinkingLevel: undefined,
}) as AgentState;
describe("AgentEditorModal", () => {
beforeEach(() => {
vi.restoreAllMocks();
cleanup();
});
it("saves avatar changes from the avatar section", async () => {
const agent = buildAgent();
const onAvatarSave = vi.fn(async () => {});
const initialBackpack = agent.avatarProfile?.accessories.backpack;
render(
createElement(AgentEditorModal, {
open: true,
client: {} as GatewayClient,
agents: [agent],
agent,
onClose: () => {},
onAvatarSave,
}),
);
fireEvent.click(screen.getByRole("button", { name: "Backpack" }));
fireEvent.click(screen.getByRole("button", { name: "Save avatar" }));
expect(onAvatarSave).toHaveBeenCalledTimes(1);
expect(onAvatarSave).toHaveBeenCalledWith(
"agent-1",
expect.objectContaining({
seed: "seed-a",
accessories: expect.objectContaining({ backpack: !initialBackpack }),
}),
);
});
it("switches to another file section", () => {
const agent = buildAgent();
render(
createElement(AgentEditorModal, {
open: true,
client: {} as GatewayClient,
agents: [agent],
agent,
onClose: () => {},
onAvatarSave: () => {},
}),
);
fireEvent.click(screen.getByRole("button", { name: /Tools/i }));
expect(screen.getByTestId("brain-panel")).toHaveTextContent("brain:agent-1:TOOLS.md");
});
it("honors the initial file section", () => {
const agent = buildAgent();
render(
createElement(AgentEditorModal, {
open: true,
client: {} as GatewayClient,
agents: [agent],
agent,
initialSection: "MEMORY.md",
onClose: () => {},
onAvatarSave: () => {},
}),
);
expect(screen.getByTestId("brain-panel")).toHaveTextContent("brain:agent-1:MEMORY.md");
});
});
+4 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { hydrateAgentFleetFromGateway } from "@/features/agents/operations/agentFleetHydration";
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
import type { StudioSettings } from "@/lib/studio/settings";
describe("hydrateAgentFleetFromGateway", () => {
@@ -13,12 +14,13 @@ describe("hydrateAgentFleetFromGateway", () => {
focused: {},
avatars: {
[gatewayUrl]: {
"agent-1": "persisted-seed",
"agent-1": createDefaultAgentAvatarProfile("persisted-seed"),
},
},
deskAssignments: {},
analytics: {},
voiceReplies: {},
office: {},
};
const call = vi.fn(async (method: string, params: unknown) => {
@@ -127,6 +129,7 @@ describe("hydrateAgentFleetFromGateway", () => {
name: "One",
sessionKey: "agent:agent-1:main",
avatarSeed: "persisted-seed",
avatarProfile: expect.objectContaining({ seed: "persisted-seed" }),
avatarUrl: "https://example.com/one.png",
model: "openai/gpt-4.1",
thinkingLevel: "medium",
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { deriveHydrateAgentFleetResult } from "@/features/agents/operations/agentFleetHydrationDerivation";
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
import type { StudioSettings } from "@/lib/studio/settings";
describe("deriveHydrateAgentFleetResult", () => {
@@ -13,12 +14,13 @@ describe("deriveHydrateAgentFleetResult", () => {
focused: {},
avatars: {
[gatewayUrl]: {
"agent-1": "persisted-seed",
"agent-1": createDefaultAgentAvatarProfile("persisted-seed"),
},
},
deskAssignments: {},
analytics: {},
voiceReplies: {},
office: {},
};
const result = deriveHydrateAgentFleetResult({
@@ -93,6 +95,7 @@ describe("deriveHydrateAgentFleetResult", () => {
name: "One",
sessionKey: "agent:agent-1:main",
avatarSeed: "persisted-seed",
avatarProfile: expect.objectContaining({ seed: "persisted-seed" }),
avatarUrl: "https://example.com/one.png",
model: "openai/gpt-4.1",
thinkingLevel: "medium",
@@ -166,6 +166,7 @@ describe("studioBootstrapOperation", () => {
deskAssignments: {},
analytics: {},
voiceReplies: {},
office: {},
}),
isFocusFilterTouched: () => false,
});
@@ -203,6 +204,7 @@ describe("studioBootstrapOperation", () => {
deskAssignments: {},
analytics: {},
voiceReplies: {},
office: {},
}),
isFocusFilterTouched: () => true,
});
@@ -157,6 +157,7 @@ describe("studioBootstrapWorkflow", () => {
deskAssignments: {},
analytics: {},
voiceReplies: {},
office: {},
};
expect(
@@ -197,6 +198,7 @@ describe("studioBootstrapWorkflow", () => {
deskAssignments: {},
analytics: {},
voiceReplies: {},
office: {},
};
expect(
+52 -9
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
import {
mergeStudioSettings,
@@ -12,6 +13,7 @@ describe("studio settings normalization", () => {
expect(normalized.gateway).toBeNull();
expect(normalized.focused).toEqual({});
expect(normalized.avatars).toEqual({});
expect(normalized.office).toEqual({});
});
it("normalizes gateway entries", () => {
@@ -114,16 +116,17 @@ describe("studio settings normalization", () => {
},
});
expect(normalized.avatars["ws://localhost:18789"]).toEqual({
"agent-1": "seed-1",
});
expect(normalized.avatars["ws://localhost:18789"]?.["agent-1"]?.seed).toBe("seed-1");
});
it("merges avatar patches", () => {
const firstProfile = createDefaultAgentAvatarProfile("seed-1");
const replacementProfile = createDefaultAgentAvatarProfile("seed-2");
const secondProfile = createDefaultAgentAvatarProfile("seed-3");
const current = normalizeStudioSettings({
avatars: {
"ws://localhost:18789": {
"agent-1": "seed-1",
"agent-1": firstProfile,
},
},
});
@@ -131,15 +134,55 @@ describe("studio settings normalization", () => {
const merged = mergeStudioSettings(current, {
avatars: {
"ws://localhost:18789": {
"agent-1": "seed-2",
"agent-2": "seed-3",
"agent-1": replacementProfile,
"agent-2": secondProfile,
},
},
});
expect(merged.avatars["ws://localhost:18789"]).toEqual({
"agent-1": "seed-2",
"agent-2": "seed-3",
expect(merged.avatars["ws://localhost:18789"]?.["agent-1"]?.seed).toBe("seed-2");
expect(merged.avatars["ws://localhost:18789"]?.["agent-2"]?.seed).toBe("seed-3");
});
it("normalizes office title preferences per gateway", () => {
const normalized = normalizeStudioSettings({
office: {
" ws://localhost:18789 ": {
title: " Team Orbit ",
},
bad: {
title: "",
},
},
});
expect(normalized.office["ws://localhost:18789"]).toEqual({
title: "Team Orbit",
});
expect(normalized.office.bad).toEqual({
title: "Luke Headquarters",
});
});
it("merges office title patches", () => {
const current = normalizeStudioSettings({
office: {
"ws://localhost:18789": {
title: "Luke Headquarters",
},
},
});
const merged = mergeStudioSettings(current, {
office: {
"ws://localhost:18789": {
title: "Orbit Control",
},
},
});
expect(merged.office["ws://localhost:18789"]).toEqual({
title: "Orbit Control",
});
});
});
+27 -7
View File
@@ -47,18 +47,18 @@ describe("studio settings route", () => {
const response = await GET();
const body = (await response.json()) as {
settings?: { gateway?: { url?: string; token?: string } | null };
localGatewayDefaults?: { url?: string; token?: string } | null;
settings?: { gateway?: { url?: string; tokenConfigured?: boolean } | null };
localGatewayDefaults?: { url?: string; tokenConfigured?: boolean } | null;
};
expect(response.status).toBe(200);
expect(body.localGatewayDefaults).toEqual({
url: "ws://localhost:18791",
token: "local-token",
tokenConfigured: true,
});
expect(body.settings?.gateway).toEqual({
url: "ws://localhost:18791",
token: "local-token",
tokenConfigured: true,
});
});
@@ -82,6 +82,11 @@ describe("studio settings route", () => {
const patch = {
gateway: { url: "ws://example.test:1234", token: "t" },
office: {
"ws://example.test:1234": {
title: "Orbit Control",
},
},
};
const putResponse = await PUT({
@@ -91,16 +96,31 @@ describe("studio settings route", () => {
const getResponse = await GET();
const body = (await getResponse.json()) as {
settings?: { gateway?: { url?: string; token?: string } | null };
settings?: {
gateway?: { url?: string; tokenConfigured?: boolean } | null;
office?: Record<string, { title?: string }>;
};
};
expect(getResponse.status).toBe(200);
expect(body.settings?.gateway).toEqual({ url: "ws://example.test:1234", token: "t" });
expect(body.settings?.gateway).toEqual({
url: "ws://example.test:1234",
tokenConfigured: true,
});
expect(body.settings?.office?.["ws://example.test:1234"]).toEqual({
title: "Orbit Control",
});
const settingsPath = path.join(tempDir, "claw3d", "settings.json");
expect(fs.existsSync(settingsPath)).toBe(true);
const raw = fs.readFileSync(settingsPath, "utf8");
const parsed = JSON.parse(raw) as { gateway?: { url?: string; token?: string } | null };
const parsed = JSON.parse(raw) as {
gateway?: { url?: string; token?: string } | null;
office?: Record<string, { title?: string }>;
};
expect(parsed.gateway).toEqual({ url: "ws://example.test:1234", token: "t" });
expect(parsed.office?.["ws://example.test:1234"]).toEqual({
title: "Orbit Control",
});
});
});