feat(coding): add conversation archive and first-message titles

This commit is contained in:
2026-09-20 12:43:51 +08:00
parent d61226532a
commit fd0293fe3d
22 changed files with 698 additions and 69 deletions

View File

@@ -205,6 +205,60 @@ describe('CodingChatPanel first Conversation', () => {
vi.resetModules();
});
it('archives the last conversation without creating another, then restores the same running conversation', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
projectApi.conversations.mockResolvedValue([conversation]);
projectApi.patch.mockImplementation(async (_id, patch) => ({
...conversation, archivedAt: patch.archived ? '2026-09-20T00:00:00Z' : null,
}));
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),
run: { status: 'running', runId: 'run-1' } });
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
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('没有最近会话,可新建对话或查看归档');
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 }));
await screen.findByText('此对话已归档,任务仍在运行。');
expect(screen.queryByRole('button', { name: '发送', exact: true })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '恢复对话' }));
await waitFor(() => expect(screen.queryByRole('button', { name: '恢复对话' })).not.toBeInTheDocument());
expect(codingConversationStore.getState().selectedConversationId).toBe(conversation.id);
expect(projectApi.patch).toHaveBeenLastCalledWith(conversation.id, { archived: false });
});
it('renames an unselected conversation from its menu without preparing that conversation', async () => {
const other = { ...conversation, id: 'unselected', title: '旧标题', updatedAt: '2025-01-01T00:00:00Z' };
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
projectApi.conversations.mockResolvedValue([conversation, other]);
projectApi.patch.mockImplementation(async (_id, patch) => ({ ...other, ...patch }));
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');
render(<CodingChatPanel />);
fireEvent.keyDown(await screen.findByRole('button', { name: '对话操作:旧标题' }), { key: 'Enter' });
fireEvent.click(await screen.findByRole('menuitem', { name: '重命名' }));
const input = screen.getByRole('textbox', { name: '对话标题' });
expect(input).toHaveValue('旧标题');
expect(input).toHaveAttribute('maxlength', '200');
fireEvent.change(input, { target: { value: ' 手动新标题 ' } });
fireEvent.submit(input.closest('form')!);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(projectApi.patch).toHaveBeenCalledWith(other.id, { title: '手动新标题' });
expect(conversationApi.snapshot).not.toHaveBeenCalledWith(other.id);
});
it('keeps a project with no Agent accessible and offers optional Agent setup', async () => {
const openProjectSettings = vi.fn();
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });