Skip to content
This repository was archived by the owner on Sep 17, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f042756
feat: add TASK_FORCE group style with live task board and SSE tracking
ginccc Jun 25, 2026
5b1bd95
fix: code review improvements for TASK_FORCE feature
ginccc Jun 25, 2026
6466e50
test: comprehensive tests for TASK_FORCE feature (62 tests)
ginccc Jun 25, 2026
9fde881
fix: harden SSE matching, add error logging, and i18n all hardcoded s…
ginccc Jun 25, 2026
9513f3f
fix: address PR review — interpolate progress label, stable keys, i18…
ginccc Jun 25, 2026
06c0a9a
fix: address CodeRabbit review — Tailwind v4 gradient, simplify SSE l…
ginccc Jun 26, 2026
319fe0c
fix: address Copilot review — SSE CRLF handling, payload compat, flow…
ginccc Jun 26, 2026
378b0c7
feat: improve groups overview and conversation history UX
ginccc Jun 26, 2026
175db88
feat: add Task Force template to group wizard
ginccc Jun 26, 2026
ce23ad7
fix: accessibility polish for groups UI
ginccc Jun 26, 2026
7149cb0
feat: enrich mock data for richer groups demo
ginccc Jun 26, 2026
23c7131
fix: resolve TS strict null errors in group-detail STATE_CONFIG
ginccc Jun 26, 2026
93f004b
feat: add sortable columns to groups overview table
ginccc Jun 26, 2026
2fa6b11
fix: render task plan JSON as structured list + move task board to top
ginccc Jun 26, 2026
4903800
fix: improve task card readability + add board toggle + persist sort
ginccc Jun 26, 2026
3f95313
fix: render ALL JSON outputs as structured cards (plan + verification)
ginccc Jun 26, 2026
8da023e
fix: remove entry-type gate from JSON detection
ginccc Jun 26, 2026
19cd70f
fix: center expand icon, add delete padding, add discussions toggle
ginccc Jun 26, 2026
05e6b33
fix: robust JSON parser for structured items (handles malformed LLM o…
ginccc Jun 26, 2026
1693f82
feat: add protocol settings to wizard, style hints, config panel impr…
ginccc Jun 27, 2026
758078c
fix: add show more toggle on task cards, make config panel scrollable
ginccc Jun 27, 2026
184875a
i18n: propagate new keys to all 10 non-English locales
ginccc Jun 27, 2026
d79b7ff
fix: update group-templates test for 6th template (task-force)
ginccc Jun 27, 2026
cd4799b
fix: address Copilot PR review feedback, bump to 6.2.0
ginccc Jun 27, 2026
b21dfe5
fix: add show more/less toggle to moderator structured items
ginccc Jun 27, 2026
0dbd4d8
feat: move panel toggles into panels, add Delete Group Only button
ginccc Jun 27, 2026
2b265e1
chore: add mandatory i18n rule to AGENTS.md, propagate latest keys
ginccc Jun 27, 2026
78c34c2
fix: add title tooltips on all truncated text, fix review issues
ginccc Jun 27, 2026
1e3ceec
fix: move panel re-open buttons from side strips to header bar
ginccc Jun 27, 2026
73e5ea3
fix: handle invalid timestamps in group list dates
ginccc Jun 27, 2026
d5499c1
fix: address Copilot review findings
ginccc Jun 27, 2026
691e629
fix: handle missing lastModifiedOn in group descriptors
ginccc Jun 27, 2026
a11080d
fix: add createdOn to GroupCard prop type
ginccc Jun 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ npm run test # All Vitest tests pass
npm run build # Production build succeeds (includes tsc -b)
```

### i18n — MANDATORY

> **⚠️ Every time you add or modify keys in `en.json`, you MUST propagate those changes to ALL 10 other locale files before committing.**

The project has **11 locales**: `en`, `de`, `fr`, `es`, `ar`, `zh`, `th`, `ja`, `ko`, `pt`, `hi`.

1. Add new keys to `src/i18n/locales/en.json` first
2. **Immediately** propagate translated versions to all other 10 locale files
3. Verify with: `Get-ChildItem src/i18n/locales/*.json | Where-Object { Select-String -Path $_ -Pattern '"YOUR_KEY"' -Quiet } | Measure-Object` → must be **11**
4. Do NOT leave this as a follow-up step — it must be done in the **same commit**

### After Completing Work

1. **Update [`HANDOFF.md`](HANDOFF.md)**: new phase row, test counts, last commit
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "eddi-manager",
"private": true,
"version": "6.1.0",
"version": "6.2.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
12 changes: 12 additions & 0 deletions src/components/groups/__tests__/discussion-transcript.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const mockConversation: GroupConversation = {
currentPhaseName: "Synthesis",
synthesizedAnswer: "### Hybrid Systems\nWe need both deterministic guardrails and LLMs.",
depth: 0,
taskList: null,
dynamicMembers: [],
createdAgentIds: [],
retainedAgentIds: [],
created: "2026-06-09T12:00:00.000Z",
lastModified: "2026-06-09T12:05:00.000Z",
};
Expand Down Expand Up @@ -175,6 +179,10 @@ describe("DiscussionTranscript", () => {
activeSpeakers: new Set(["agent-2"]),
synthesizedAnswer: null,
error: null,
taskPlan: null,
taskVerifications: new Map(),
tasksInProgress: new Set(),
tasksCompleted: new Set(),
};

renderWithProviders(
Expand Down Expand Up @@ -205,6 +213,10 @@ describe("DiscussionTranscript", () => {
activeSpeakers: new Set(),
synthesizedAnswer: null,
error: "SSE Connection Aborted",
taskPlan: null,
taskVerifications: new Map(),
tasksInProgress: new Set(),
tasksCompleted: new Set(),
};

renderWithProviders(
Expand Down
144 changes: 144 additions & 0 deletions src/components/groups/__tests__/group-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,148 @@ describe("GroupCard", () => {
await user.click(screen.getByText("Duplicate"));
expect(screen.queryByText("Duplicate")).not.toBeInTheDocument();
});

// ─── Style badge variants ──────────────────────────────────────────

it("renders style badge with icon and label for ROUND_TABLE", () => {
renderWithProviders(
<GroupCard group={baseGroup} style="ROUND_TABLE" />
);
expect(screen.getByText("Round Table")).toBeInTheDocument();
expect(screen.getByText("🗣️")).toBeInTheDocument();
});

it("renders style badge for TASK_FORCE", () => {
renderWithProviders(
<GroupCard group={baseGroup} style="TASK_FORCE" />
);
expect(screen.getByText("Task Force")).toBeInTheDocument();
expect(screen.getByText("🎯")).toBeInTheDocument();
});

// ─── Member preview chips ─────────────────────────────────────────

it("renders member preview chips (up to 4)", () => {
const members = [
{ displayName: "Agent Alpha", memberType: "AGENT" },
{ displayName: "Agent Beta", memberType: "AGENT" },
{ displayName: "Sub Group", memberType: "GROUP" },
];
renderWithProviders(
<GroupCard group={baseGroup} members={members} memberCount={3} />
);
expect(screen.getByText("Agent Alpha")).toBeInTheDocument();
expect(screen.getByText("Agent Beta")).toBeInTheDocument();
expect(screen.getByText("Sub Group")).toBeInTheDocument();
});

it("shows Bot icon for AGENT type members", () => {
const members = [{ displayName: "Agent Alpha", memberType: "AGENT" }];
renderWithProviders(
<GroupCard group={baseGroup} members={members} memberCount={1} />
);
// Bot icon from lucide has aria-hidden="true" on SVG inside the chip
const chip = screen.getByRole("listitem", { name: "Agent Alpha" });
const svg = chip.querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("aria-hidden", "true");
});

it("shows Users icon for GROUP type members", () => {
const members = [{ displayName: "Sub Group", memberType: "GROUP" }];
renderWithProviders(
<GroupCard group={baseGroup} members={members} memberCount={1} />
);
const chip = screen.getByRole("listitem", { name: "Sub Group" });
const svg = chip.querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("aria-hidden", "true");
});

it("shows overflow count when more than 4 members", () => {
const members = [
{ displayName: "A1", memberType: "AGENT" },
{ displayName: "A2", memberType: "AGENT" },
{ displayName: "A3", memberType: "AGENT" },
{ displayName: "A4", memberType: "AGENT" },
{ displayName: "A5", memberType: "AGENT" },
{ displayName: "A6", memberType: "AGENT" },
];
renderWithProviders(
<GroupCard group={baseGroup} members={members} memberCount={6} />
);
// Only first 4 chips rendered; overflow text shows "+2 more"
expect(screen.queryByText("A5")).not.toBeInTheDocument();
expect(screen.queryByText("A6")).not.toBeInTheDocument();
expect(screen.getByText(/\+2 more/)).toBeInTheDocument();
});

it("does not render member chips when members is empty", () => {
renderWithProviders(
<GroupCard group={baseGroup} members={[]} memberCount={0} />
);
expect(screen.queryByRole("list")).not.toBeInTheDocument();
});

it("member chips have role='listitem' and aria-label", () => {
const members = [
{ displayName: "Agent Alpha", memberType: "AGENT" },
{ displayName: "Agent Beta", memberType: "AGENT" },
];
renderWithProviders(
<GroupCard group={baseGroup} members={members} memberCount={2} />
);
const items = screen.getAllByRole("listitem");
expect(items.length).toBe(2);
expect(items[0]).toHaveAttribute("aria-label", "Agent Alpha");
expect(items[1]).toHaveAttribute("aria-label", "Agent Beta");
});

it("member container has role='list' and data-testid", () => {
const members = [{ displayName: "Agent Alpha", memberType: "AGENT" }];
renderWithProviders(
<GroupCard group={baseGroup} members={members} memberCount={1} />
);
const list = screen.getByRole("list");
expect(list).toHaveAttribute("data-testid", "group-card-members-grp-123");
});

// ─── Name as link ─────────────────────────────────────────────────

it("renders group name as a navigable link", () => {
renderWithProviders(<GroupCard group={baseGroup} />);
const link = screen.getByText("Product Review Panel").closest("a");
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "/manage/groups/grp-123?version=2");
});

// ─── Relative time ────────────────────────────────────────────────

it("renders relative time", () => {
// baseGroup.lastModifiedOn is 1 hour ago
renderWithProviders(<GroupCard group={baseGroup} />);
// formatRelativeTime returns something like "1h ago" — just verify something renders
const card = screen.getByTestId("group-card-grp-123");
// The footer area should contain time text (not empty)
const timeSpan = card.querySelector("[title]");
expect(timeSpan).toBeInTheDocument();
});

it("renders '—' when lastModifiedOn is 0", () => {
const group = { ...baseGroup, lastModifiedOn: 0 };
renderWithProviders(<GroupCard group={group} />);
expect(screen.getByText("—")).toBeInTheDocument();
});

it("renders '—' when lastModifiedOn is undefined", () => {
const group = { ...baseGroup, lastModifiedOn: undefined as unknown as number };
renderWithProviders(<GroupCard group={group} />);
expect(screen.getByText("—")).toBeInTheDocument();
});

it("renders valid relative time for valid timestamp", () => {
const group = { ...baseGroup, lastModifiedOn: Date.now() - 7200000 }; // 2h ago
renderWithProviders(<GroupCard group={group} />);
expect(screen.getByText("2h ago")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe("GroupConfigPanel", () => {
expect(screen.getByText("Timeout")).toBeInTheDocument();
expect(screen.getByText("30s")).toBeInTheDocument();
expect(screen.getByText("On Failure")).toBeInTheDocument();
expect(screen.getAllByText("SKIP")).toHaveLength(2);
expect(screen.getAllByText("Skip")).toHaveLength(2);
expect(screen.getByText("Max Retries")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("Max Rounds")).toBeInTheDocument();
Expand Down
Loading
Loading