feat: organize project conversations and add coding teacher side chat

This commit is contained in:
2026-09-22 12:30:26 +08:00
parent 2f82b9f79c
commit 9242a40d87
45 changed files with 2987 additions and 1137 deletions

View File

@@ -43,7 +43,8 @@ function withForkSource(
};
}
const runtimeSnapshot = deferred<ConversationSnapshot>();
const navigate = vi.hoisted(() => vi.fn());
vi.mock('react-router-dom', async (importOriginal) => ({ ...await importOriginal<typeof import('react-router-dom')>(), useNavigate: () => navigate }));
class FakeEventSource {
onopen: ((event: Event) => void) | null = null;
@@ -52,7 +53,6 @@ class FakeEventSource {
addEventListener = vi.fn();
}
const source = new FakeEventSource();
const project: CodingProjectSummary = {
id: 'project-1',
name: 'Local project',
@@ -216,18 +216,20 @@ describe('CodingChatPanel first Conversation', () => {
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockResolvedValue({ ...createLocalConversationSnapshot(project.id, conversation),
run: { status: 'running', runId: 'run-1' } });
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { codingConversationStore } = await import('@/stores/coding-conversations');
render(<CodingChatPanel />);
const menu = await screen.findByRole('button', { name: '对话操作:新对话' });
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId).toBe(conversation.id));
fireEvent.keyDown(menu, { key: 'Enter' });
fireEvent.click(await screen.findByRole('menuitem', { name: '归档(任务继续运行)' }));
await screen.findByText('没有最近会话,可新建对话或查看归档');
await screen.findByTestId('coding-conversation-pending');
expect(projectApi.create).not.toHaveBeenCalled();
expect(conversationApi.abort).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '已归档(1)' }));
fireEvent.click(within(screen.getByTestId('coding-conversation-sidebar')).getByRole('button', { name: '新对话', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '查看归档对话' }));
fireEvent.click(within(screen.getByTestId('project-conversations')).getByRole('button', { name: '新对话', exact: true }));
await screen.findByText('此对话已归档,任务仍在运行。');
expect(screen.queryByRole('button', { name: '发送', exact: true })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '恢复对话' }));
@@ -245,7 +247,9 @@ describe('CodingChatPanel first Conversation', () => {
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
render(<CodingChatPanel />);
fireEvent.keyDown(await screen.findByRole('button', { name: '对话操作:旧标题' }), { key: 'Enter' });
fireEvent.click(await screen.findByRole('menuitem', { name: '重命名' }));
@@ -267,13 +271,15 @@ describe('CodingChatPanel first Conversation', () => {
config: { ...configForAgents([]), initialized: false },
});
projectApi.conversations.mockResolvedValue([]);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
render(<CodingChatPanel onOpenProjectSettings={openProjectSettings} />);
expect(await screen.findByTestId('coding-chat-empty-agent')).toHaveTextContent('项目已准备好');
expect(projectApi.create).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '创建项目智能体' }));
fireEvent.click(within(screen.getByTestId('coding-chat-empty-agent')).getByRole('button', { name: '项目设置' }));
expect(openProjectSettings).toHaveBeenCalledOnce();
});
@@ -282,7 +288,9 @@ describe('CodingChatPanel first Conversation', () => {
const onCreateProject = vi.fn();
const onOpenProject = vi.fn();
const onOpenProjectSettings = vi.fn();
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
render(
<CodingChatPanel
@@ -311,44 +319,22 @@ describe('CodingChatPanel first Conversation', () => {
expect(onOpenProjectSettings).not.toHaveBeenCalled();
});
it('makes the first-Conversation textarea editable while runtime metadata is held', async () => {
it('keeps an empty project editable without creating a conversation until first send', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
projectApi.conversations.mockResolvedValue([]);
projectApi.create.mockResolvedValue(conversation);
conversationApi.snapshot.mockReturnValue(runtimeSnapshot.promise);
conversationApi.events.mockResolvedValue(source as unknown as EventSource);
conversationApi.submit.mockRejectedValue(new Error('Provider is intentionally not used'));
conversationApi.recover.mockResolvedValue(undefined);
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { createLocalConversationSnapshot } = await import(
'@/pages/Chat/coding-chat-snapshot'
);
const view = render(<CodingChatPanel />);
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
render(<CodingChatPanel />);
const textbox = await screen.findByRole('textbox');
expect(screen.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: '麦洛 M 标识' })).toBeInTheDocument();
expect(textbox).toBeEnabled();
expect(projectApi.create).toHaveBeenCalledOnce();
expect(conversationApi.snapshot).toHaveBeenCalledWith(conversation.id);
fireEvent.change(textbox, { target: { value: 'First prompt' } });
const sendButton = screen.getByRole('button', { name: '发送' });
await waitFor(() => expect(sendButton).toBeEnabled());
expect(textbox).toHaveValue('First prompt');
expect(conversationApi.submit).not.toHaveBeenCalled();
await act(async () => {
runtimeSnapshot.resolve({
...createLocalConversationSnapshot(project.id, conversation),
worker: { status: 'ready', generation: 1 },
cursor: { workerGeneration: 1, seq: 0 },
});
await runtimeSnapshot.promise;
});
view.unmount();
expect(source.close).toHaveBeenCalled();
expect(projectApi.create).not.toHaveBeenCalled();
expect(conversationApi.snapshot).not.toHaveBeenCalled();
fireEvent.change(textbox, {target:{value:'First prompt'}});
fireEvent.click(screen.getByRole('button',{name:'发送'}));
await waitFor(()=>expect(projectApi.create).toHaveBeenCalledOnce());
});
it('rehydrates a preselected stale Conversation when the programming view mounts', async () => {
@@ -357,7 +343,9 @@ describe('CodingChatPanel first Conversation', () => {
projectApi.conversations.mockResolvedValue([conversation]);
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
const { codingWorkspaceStore } = await import('@/stores/coding-workspace');
@@ -427,7 +415,9 @@ describe('CodingChatPanel first Conversation', () => {
.mockResolvedValueOnce(firstSource as unknown as EventSource)
.mockResolvedValueOnce(resumedSource as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
const localSnapshot = createLocalConversationSnapshot(project.id, conversation);
@@ -461,7 +451,7 @@ describe('CodingChatPanel first Conversation', () => {
await waitFor(() => expect(
codingConversationStore.getState().entriesByConversationId[conversation.id]?.reducer.snapshot?.run.status,
).toBe('running'));
expect(screen.getByText('处理中')).toBeInTheDocument();
expect(screen.getByLabelText('对话正在运行')).toBeInTheDocument();
const snapshotCallsBeforeSleep = conversationApi.snapshot.mock.calls.length;
conversationApi.snapshot.mockResolvedValue(terminalSnapshot);
@@ -495,50 +485,15 @@ describe('CodingChatPanel first Conversation', () => {
}
});
it('does not let a slow first-Conversation completion steal selection after an Agent switch', async () => {
const slowCreate = deferred<CodingConversationMetadata>();
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config: configForAgents([agent, reviewer]) });
projectApi.conversations.mockResolvedValue([reviewerConversation]);
projectApi.create.mockReturnValue(slowCreate.promise);
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.submit.mockRejectedValue(new Error('Provider is intentionally not used'));
conversationApi.recover.mockResolvedValue(undefined);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
createLocalConversationSnapshot(
project.id,
conversationId === reviewerConversation.id ? reviewerConversation : conversation,
)
));
render(<CodingChatPanel />);
await screen.findByRole('textbox');
await waitFor(() => expect(projectApi.create).toHaveBeenCalledWith({
projectId: project.id,
agentId: agent.id,
title: '新对话',
}));
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
.toBe(reviewerConversation.id));
await act(async () => {
slowCreate.resolve(conversation);
await slowCreate.promise;
});
expect(codingConversationStore.getState().selectedConversationId).toBe(reviewerConversation.id);
});
it('renders Conversations as children of only the selected Agent', async () => {
it('lists all project conversations without Agent navigation', 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 { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
createLocalConversationSnapshot(
@@ -548,22 +503,12 @@ describe('CodingChatPanel first 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();
const list = await screen.findByRole('group', {name:'项目会话'});
expect(within(list).getByRole('button',{name:conversation.title})).toBeVisible();
expect(within(list).getByRole('button',{name:reviewerConversation.title})).toBeVisible();
expect(screen.queryByRole('button',{name:agent.name})).not.toBeInTheDocument();
fireEvent.click(within(list).getByRole('button',{name:reviewerConversation.title}));
await waitFor(()=>expect(within(list).getByRole('button',{name:reviewerConversation.title})).toHaveAttribute('aria-current','page'));
});
it('does not let a pending message fork steal selection after a Conversation switch', async () => {
@@ -574,7 +519,9 @@ describe('CodingChatPanel first Conversation', () => {
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
conversationApi.fork.mockReturnValue(forkFlight.promise);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
@@ -590,7 +537,7 @@ describe('CodingChatPanel first Conversation', () => {
fireEvent.click(screen.getByRole('button', { name: '从这里创建新对话分支' }));
await waitFor(() => expect(conversationApi.fork)
.toHaveBeenCalledWith(conversation.id, `entry-${conversation.id}`));
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
fireEvent.click(screen.getByRole('button', { name: reviewerConversation.title }));
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
.toBe(reviewerConversation.id));
expect(screen.getByRole('button', { name: '从这里创建新对话分支' })).toBeEnabled();
@@ -622,7 +569,9 @@ describe('CodingChatPanel first Conversation', () => {
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
conversationApi.fork.mockReturnValue(forkFlight.promise);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingWorkspaceStore } = await import('@/stores/coding-workspace');
const { codingConversationStore } = await import('@/stores/coding-conversations');
@@ -664,7 +613,9 @@ describe('CodingChatPanel first Conversation', () => {
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 { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingWorkspaceStore } = await import('@/stores/coding-workspace');
const { codingConversationStore } = await import('@/stores/coding-conversations');
@@ -686,7 +637,7 @@ describe('CodingChatPanel first Conversation', () => {
});
expect(await screen.findByText('metadata rejected')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
fireEvent.click(screen.getByRole('button', { name: reviewerConversation.title }));
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
.toBe(reviewerConversation.id));
expect(screen.queryByText('metadata rejected')).not.toBeInTheDocument();
@@ -698,7 +649,9 @@ describe('CodingChatPanel first Conversation', () => {
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 { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
@@ -720,7 +673,7 @@ describe('CodingChatPanel first Conversation', () => {
expect(screen.getAllByRole('button', { name: '项目设置' })).toHaveLength(1);
fireEvent.click(screen.getByRole('button', { name: '项目设置' }));
expect(onOpenProjectSettings).toHaveBeenCalledOnce();
expect(navigate).toHaveBeenCalledWith('/project-config');
});
it('remounts pending interaction state when switching Conversations', async () => {
@@ -731,7 +684,9 @@ describe('CodingChatPanel first Conversation', () => {
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
conversationApi.respond.mockReturnValue(respondFlight.promise);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => {
@@ -757,7 +712,7 @@ describe('CodingChatPanel first Conversation', () => {
interactionId: `interaction-${conversation.id}`,
confirmed: true,
}));
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
fireEvent.click(screen.getByRole('button', { name: reviewerConversation.title }));
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
.toBe(reviewerConversation.id));
expect(await screen.findByText(`Confirm ${reviewerConversation.id}`)).toBeInTheDocument();
@@ -778,7 +733,9 @@ describe('CodingChatPanel first Conversation', () => {
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.submit.mockReturnValueOnce(pendingSubmit.promise);
conversationApi.recover.mockResolvedValue(undefined);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
@@ -802,7 +759,7 @@ describe('CodingChatPanel first Conversation', () => {
fireEvent.click(screen.getByRole('button', { name: '发送' }));
await waitFor(() => expect(conversationApi.submit).toHaveBeenCalledOnce());
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
fireEvent.click(screen.getByRole('button', { name: reviewerConversation.title }));
await waitFor(() => expect(screen.getByRole('textbox')).toHaveValue(''));
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'B prompt' } });
await waitFor(() => expect(screen.getByRole('button', { name: '发送' })).toBeEnabled());
@@ -835,7 +792,9 @@ describe('CodingChatPanel first Conversation', () => {
runId: 'run-retry',
mode: input.mode,
}));
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
@@ -871,7 +830,9 @@ describe('CodingChatPanel first Conversation', () => {
undefined,
{ backendCode: 'CODING_REQUEST_UNCERTAIN' },
));
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
@@ -949,7 +910,9 @@ describe('CodingChatPanel first Conversation', () => {
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
conversationApi.recover.mockResolvedValue(undefined);
conversationApi.abort.mockResolvedValue(undefined);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const runningSnapshot: ConversationSnapshot = {
...createLocalConversationSnapshot(project.id, conversation),
@@ -1018,7 +981,9 @@ describe('CodingChatPanel first Conversation', () => {
byteLength: file.size,
}));
});
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
@@ -1083,7 +1048,9 @@ describe('CodingChatPanel first Conversation', () => {
? Promise.reject(new Error('图片附件无效。'))
: upload.promise
));
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
@@ -1131,7 +1098,9 @@ describe('CodingChatPanel first Conversation', () => {
mime: file.type,
byteLength: file.size,
}));
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
@@ -1191,7 +1160,9 @@ describe('CodingChatPanel first Conversation', () => {
byteLength: file.size,
};
});
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
@@ -1240,7 +1211,9 @@ describe('CodingChatPanel first Conversation', () => {
runId: 'run-1',
mode: input.mode,
}));
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingChatPanel: CoreChatPanel } = await import('@/pages/Chat/CodingChatPanel');
const { CodingProjectConversations } = await import('@/pages/Chat/CodingProjectConversations');
const CodingChatPanel = (props: import('@/pages/Chat/CodingChatPanel').CodingChatPanelProps) => <><CodingProjectConversations projectId={project.id} /><CoreChatPanel {...props} /></>;
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
const { codingConversationStore } = await import('@/stores/coding-conversations');
const runningSnapshot: ConversationSnapshot = {