diff --git a/src/lib/components/AgentMonitorPanel.svelte b/src/lib/components/AgentMonitorPanel.svelte
index 9fcd85b..ef7770c 100644
--- a/src/lib/components/AgentMonitorPanel.svelte
+++ b/src/lib/components/AgentMonitorPanel.svelte
@@ -270,6 +270,14 @@
/>
{/if}
+
+
+ {agent.characterName}
+
{
const conversationId = "test-conversation-1";
const otherConversationId = "test-conversation-2";
- const createMockAgent = (overrides?: Partial): AgentInfo => ({
+ type AgentInput = Omit;
+
+ const createMockAgent = (overrides?: Partial): AgentInput => ({
toolUseId: "toolu_test123",
description: "Test agent",
subagentType: "Explore",
@@ -37,7 +40,29 @@ describe("agents store", () => {
const agents = get(getAgentsForConversation(conversationId));
expect(agents).toHaveLength(1);
- expect(agents[0]).toEqual(agent);
+ expect(agents[0]).toMatchObject(agent);
+ });
+
+ it("assigns a character name and avatar to added agents", () => {
+ const agent = createMockAgent();
+ agentStore.addAgent(conversationId, agent);
+
+ const agents = get(getAgentsForConversation(conversationId));
+ const validNames = CHARACTER_POOL.map((c) => c.name);
+ expect(validNames).toContain(agents[0].characterName);
+ expect(agents[0].characterAvatar).toMatch(/^https:\/\//u);
+ });
+
+ it("avoids duplicate character names across agents when possible", () => {
+ // Add 6 agents - each should ideally get a unique character
+ for (let i = 0; i < 6; i++) {
+ agentStore.addAgent(conversationId, createMockAgent({ toolUseId: `tool${i.toString()}` }));
+ }
+
+ const agents = get(getAgentsForConversation(conversationId));
+ const names = agents.map((a) => a.characterName);
+ const uniqueNames = new Set(names);
+ expect(uniqueNames.size).toBe(6);
});
it("adds multiple agents to the same conversation", () => {
@@ -49,8 +74,8 @@ describe("agents store", () => {
const agents = get(getAgentsForConversation(conversationId));
expect(agents).toHaveLength(2);
- expect(agents[0]).toEqual(agent1);
- expect(agents[1]).toEqual(agent2);
+ expect(agents[0]).toMatchObject(agent1);
+ expect(agents[1]).toMatchObject(agent2);
});
it("keeps agents in different conversations separate", () => {
@@ -65,8 +90,8 @@ describe("agents store", () => {
expect(agents1).toHaveLength(1);
expect(agents2).toHaveLength(1);
- expect(agents1[0]).toEqual(agent1);
- expect(agents2[0]).toEqual(agent2);
+ expect(agents1[0]).toMatchObject(agent1);
+ expect(agents2[0]).toMatchObject(agent2);
});
});
@@ -256,7 +281,7 @@ describe("agents store", () => {
expect(agents1).toHaveLength(0);
expect(agents2).toHaveLength(1);
- expect(agents2[0]).toEqual(agent2);
+ expect(agents2[0]).toMatchObject(agent2);
});
it("does nothing if conversation doesn't exist", () => {
diff --git a/src/lib/stores/agents.ts b/src/lib/stores/agents.ts
index 989e103..406a4ca 100644
--- a/src/lib/stores/agents.ts
+++ b/src/lib/stores/agents.ts
@@ -1,5 +1,6 @@
import { writable, derived } from "svelte/store";
import type { AgentInfo } from "$lib/types/agents";
+import { assignCharacter } from "$lib/utils/agentCharacters";
// Map of conversation ID -> agents in that conversation
const agentsByConversation = writable>({});
@@ -8,12 +9,17 @@ function createAgentStore() {
return {
subscribe: agentsByConversation.subscribe,
- addAgent(conversationId: string, agent: AgentInfo) {
+ addAgent(conversationId: string, agent: Omit) {
agentsByConversation.update((state) => {
const existing = state[conversationId] || [];
+ const activeNames = existing.map((a) => a.characterName);
+ const character = assignCharacter(activeNames);
return {
...state,
- [conversationId]: [...existing, agent],
+ [conversationId]: [
+ ...existing,
+ { ...agent, characterName: character.name, characterAvatar: character.avatar },
+ ],
};
});
},
diff --git a/src/lib/types/agents.ts b/src/lib/types/agents.ts
index 870e674..2cb53cd 100644
--- a/src/lib/types/agents.ts
+++ b/src/lib/types/agents.ts
@@ -10,6 +10,8 @@ export interface AgentInfo {
status: AgentStatus;
parentToolUseId?: string;
durationMs?: number;
+ characterName: string;
+ characterAvatar: string;
}
export interface AgentStartPayload {
diff --git a/src/lib/utils/agentCharacters.test.ts b/src/lib/utils/agentCharacters.test.ts
new file mode 100644
index 0000000..67d6562
--- /dev/null
+++ b/src/lib/utils/agentCharacters.test.ts
@@ -0,0 +1,69 @@
+import { describe, it, expect } from "vitest";
+import { CHARACTER_POOL, assignCharacter } from "./agentCharacters";
+
+describe("agentCharacters", () => {
+ describe("CHARACTER_POOL", () => {
+ it("contains exactly 6 characters", () => {
+ expect(CHARACTER_POOL).toHaveLength(6);
+ });
+
+ it("each character has a name and avatar", () => {
+ for (const character of CHARACTER_POOL) {
+ expect(character.name).toBeTruthy();
+ expect(character.avatar).toBeTruthy();
+ expect(character.avatar).toMatch(/^https:\/\//u);
+ }
+ });
+
+ it("all names are unique", () => {
+ const names = CHARACTER_POOL.map((c) => c.name);
+ const uniqueNames = new Set(names);
+ expect(uniqueNames.size).toBe(CHARACTER_POOL.length);
+ });
+ });
+
+ describe("assignCharacter", () => {
+ it("returns a character from the pool", () => {
+ const character = assignCharacter([]);
+ const names = CHARACTER_POOL.map((c) => c.name);
+ expect(names).toContain(character.name);
+ });
+
+ it("avoids names already in use when possible", () => {
+ const takenNames = ["Amari", "Keiko", "Minori", "Reina", "Tatsumi"];
+ // Run many times to confirm we never get a taken name
+ for (let i = 0; i < 50; i++) {
+ const character = assignCharacter(takenNames);
+ expect(takenNames).not.toContain(character.name);
+ expect(character.name).toBe("Yumiko");
+ }
+ });
+
+ it("picks from the full pool when all 6 names are taken", () => {
+ const allNames = CHARACTER_POOL.map((c) => c.name);
+ const seen = new Set();
+ // Run enough times that we'd statistically see variety
+ for (let i = 0; i < 100; i++) {
+ const character = assignCharacter(allNames);
+ seen.add(character.name);
+ }
+ // Should still pick valid characters
+ for (const name of seen) {
+ expect(allNames).toContain(name);
+ }
+ // With 100 runs and 6 characters, we should see at least 2 distinct names
+ expect(seen.size).toBeGreaterThan(1);
+ });
+
+ it("returns a character with both name and avatar", () => {
+ const character = assignCharacter([]);
+ expect(character.name).toBeTruthy();
+ expect(character.avatar).toBeTruthy();
+ });
+
+ it("works when the active list is empty", () => {
+ const character = assignCharacter([]);
+ expect(character).toBeDefined();
+ });
+ });
+});
diff --git a/src/lib/utils/agentCharacters.ts b/src/lib/utils/agentCharacters.ts
new file mode 100644
index 0000000..62de584
--- /dev/null
+++ b/src/lib/utils/agentCharacters.ts
@@ -0,0 +1,23 @@
+export interface AgentCharacter {
+ name: string;
+ avatar: string;
+}
+
+export const CHARACTER_POOL: readonly AgentCharacter[] = [
+ { name: "Amari", avatar: "https://cdn.nhcarrigan.com/amari.png" },
+ { name: "Keiko", avatar: "https://cdn.nhcarrigan.com/keiko.png" },
+ { name: "Minori", avatar: "https://cdn.nhcarrigan.com/minori.png" },
+ { name: "Reina", avatar: "https://cdn.nhcarrigan.com/reina.png" },
+ { name: "Tatsumi", avatar: "https://cdn.nhcarrigan.com/tatsumi.png" },
+ { name: "Yumiko", avatar: "https://cdn.nhcarrigan.com/yumiko.png" },
+];
+
+/**
+ * Picks a character for a new subagent.
+ * Avoids names already assigned to active agents unless all six are taken.
+ */
+export function assignCharacter(activeNames: readonly string[]): AgentCharacter {
+ const available = CHARACTER_POOL.filter((c) => !activeNames.includes(c.name));
+ const pool = available.length > 0 ? available : [...CHARACTER_POOL];
+ return pool[Math.floor(Math.random() * pool.length)];
+}