feat(coding): nest Conversations under Agents

This commit is contained in:
2026-08-25 16:34:11 +08:00
parent c4ca16fa19
commit 45d933732a
5 changed files with 238 additions and 64 deletions

View File

@@ -448,6 +448,11 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
performance.mark('pi-first-chat-start');
window.location.hash = '/chat';
});
const builderButton = page.getByRole('button', { name: 'Builder', exact: true });
await expect(builderButton).toHaveAttribute('aria-expanded', 'true');
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
await expect(builderConversations).toBeVisible();
await expect(builderConversations.getByRole('button', { name: '新对话', exact: true })).toBeVisible();
const composer = page.getByRole('textbox');
await expect(composer).toBeEnabled();
const editableMs = await page.evaluate(() => (
@@ -560,7 +565,9 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
await page.getByRole('button', { name: /\/review/ }).click();
await expect(page.getByRole('textbox')).toHaveValue('/review ');
await page.getByRole('button', { name: 'Second Conversation' }).first().click();
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
await expect(builderConversations).toBeVisible();
await builderConversations.getByRole('button', { name: 'Second Conversation' }).click();
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
await expect(page.getByRole('combobox', { name: '当前对话模型' })).toHaveValue(
JSON.stringify(['account-e2e', 'model-b']),

View File

@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -267,6 +267,40 @@ describe('CodingChatPanel first Conversation', () => {
expect(codingConversationStore.getState().selectedConversationId).toBe(reviewerConversation.id);
});
it('renders Conversations as children of only the selected Agent', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config: configForAgents([agent, reviewer]) });
projectApi.conversations.mockResolvedValue([conversation, reviewerConversation]);
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
createLocalConversationSnapshot(
project.id,
conversationId === reviewerConversation.id ? reviewerConversation : conversation,
)
));
render(<CodingChatPanel />);
const builderButton = await screen.findByRole('button', { name: agent.name });
const reviewerButton = screen.getByRole('button', { name: reviewer.name });
expect(builderButton).toHaveAttribute('aria-expanded', 'true');
expect(reviewerButton).toHaveAttribute('aria-expanded', 'false');
const builderConversations = screen.getByRole('group', { name: `${agent.name} 的对话` });
expect(within(builderConversations).getByRole('button', { name: conversation.title })).toBeVisible();
expect(screen.queryByText(reviewerConversation.title)).not.toBeInTheDocument();
fireEvent.click(reviewerButton);
await waitFor(() => expect(reviewerButton).toHaveAttribute('aria-expanded', 'true'));
expect(builderButton).toHaveAttribute('aria-expanded', 'false');
const reviewerConversations = screen.getByRole('group', { name: `${reviewer.name} 的对话` });
expect(within(reviewerConversations)
.getByRole('button', { name: reviewerConversation.title })).toBeVisible();
expect(screen.queryByText(conversation.title)).not.toBeInTheDocument();
});
it('does not let a pending fork steal selection or Header busy state after a Conversation switch', async () => {
const forkFlight = deferred<CodingConversationMetadata>();
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });