feat(agents): move conversation attachments into a header side panel

This commit is contained in:
鲨鱼辣椒 committed 2026-10-12 23:12:24 +08:00
1 parent e9f204776a
commit 0d54690dd6
10 files changed
+589 -98

No files matched your search

+46 -7
View File
@@ -42,6 +42,11 @@ beforeEach(() => {
});
afterEach(cleanup);
async function chooseFileFromMenu(button = screen.getByRole('button', { name: '添加文件或图片' })) {
fireEvent.keyDown(button, { key: 'ArrowDown' });
fireEvent.click(await screen.findByRole('menuitem', { name: '添加文件或图片', exact: true }));
}
it('recovers a server queue and cancels it after remount without resubmitting messages', async () => {
const base = api.call.getMockImplementation()!;
let cancelled = false;
@@ -383,12 +388,12 @@ it('creates an attachment conversation only after a native pick and deselects th
render(<CloudChat slug={slug} />);
const add = screen.getByRole('button', { name: '添加文件或图片' });
await waitFor(() => expect(add).toBeEnabled());
fireEvent.click(add);
await chooseFileFromMenu(add);
await waitFor(() => expect(api.upload).toHaveBeenCalledTimes(1));
await waitFor(() => expect(add).toBeEnabled());
expect(api.call.mock.calls.some(call => call[0] === 'createThread')).toBe(false);
expect(screen.queryByLabelText('历史对话')).not.toBeInTheDocument();
fireEvent.click(add);
await chooseFileFromMenu(add);
await waitFor(() => expect(api.call).toHaveBeenCalledWith('confirmAttachment', {
thread_id: 'thread-1', attachments: [{ object_name: 'temporary/notes.txt', file_type: 'txt' }],
}));
@@ -400,8 +405,15 @@ it('creates an attachment conversation only after a native pick and deselects th
fireEvent.click(remove);
expect(screen.queryByRole('button', { name: '移除附件 notes.txt' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '发送', exact: true })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '对话文件' }));
expect(await screen.findByRole('checkbox', { name: '选择附件 notes.txt' })).not.toBeChecked();
fireEvent.click(screen.getByRole('button', { name: '附件,共 1 个文件' }));
const checkbox = await screen.findByRole('checkbox', { name: '选择附件 notes.txt' });
expect(checkbox).not.toBeChecked();
fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
fireEvent.click(screen.getByRole('button', { name: '返回聊天 · 已选 1 个附件' }));
expect(await screen.findByRole('button', { name: '移除附件 notes.txt' })).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '移除附件 notes.txt' }));
expect(screen.queryByRole('button', { name: '移除附件 notes.txt' })).not.toBeInTheDocument();
expect(api.call.mock.calls.some(call => ['deleteAttachment', 'submit', 'preview'].includes(call[0]))).toBe(false);
});
@@ -419,7 +431,7 @@ it('blocks preview switching while creating an attachment thread and ignores a l
api.upload.mockResolvedValue({ object_name: 'temporary/old-agent-notes.txt', file_name: 'old-agent-notes.txt', file_type: 'txt', parse_supported: false, parse_methods: [] });
const view = render(<CloudChat slug={slug} previewRevision={1} />);
await waitFor(() => expect(screen.getByRole('button', { name: '添加文件或图片' })).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: '添加文件或图片' }));
await chooseFileFromMenu();
await waitFor(() => expect(api.call.mock.calls.filter(call => call[0] === 'createThread')).toHaveLength(1));
expect(screen.getByRole('button', { name: '新对话' })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '新对话' }));
@@ -456,7 +468,7 @@ it('retries the same picked attachment and thread after confirmation fails, then
api.upload.mockResolvedValue({ object_name: 'temporary/notes.txt', file_name: 'notes.txt', file_type: 'txt', parse_supported: false, parse_methods: [] });
render(<CloudChat slug={slug} />);
await waitFor(() => expect(screen.getByRole('button', { name: '添加文件或图片' })).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: '添加文件或图片' }));
await chooseFileFromMenu();
const retry = await screen.findByRole('button', { name: '重试添加' });
fireEvent.change(screen.getByLabelText('消息'), { target: { value: '请阅读这个附件' } });
expect(screen.getByRole('button', { name: '发送', exact: true })).toBeDisabled();
@@ -490,7 +502,7 @@ it('cancels an unconfirmed attachment without deleting it and permits the typed
render(<CloudChat slug={slug} />);
await waitFor(() => expect(screen.getByRole('button', { name: '添加文件或图片' })).toBeEnabled());
fireEvent.change(screen.getByLabelText('消息'), { target: { value: '继续讨论我的想法' } });
fireEvent.click(screen.getByRole('button', { name: '添加文件或图片' }));
await chooseFileFromMenu();
await screen.findByRole('button', { name: '重试添加' });
expect(screen.getByRole('button', { name: '发送', exact: true })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '取消添加' }));
@@ -569,3 +581,30 @@ it('does not mark a retained hidden conversation as read until it is shown', asy
view.rerender(<CloudChat slug={slug} activeView />);
await waitFor(() => expect(api.call).toHaveBeenCalledWith('viewed', { thread_id: 'thread-1', run_id: 'run-1' }));
});
it('discovers generated files after the authoritative completed reply without reopening the conversation', async () => {
const base = api.call.getMockImplementation()!;
const events = new Stream();
api.events.mockResolvedValue(events);
let completed = false;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'threads') return { threads: [{ thread_id: 'thread-1', client_thread_id: 'client', mode: 'published' }], next_offset: null };
if (operation === 'history') return { ...history, messages: completed ? [{ id: 'answer-1', role: 'assistant', content: '故事文件已做好', run_id: 'run-1' }] : [], run: { ...run, status: completed ? 'completed' : 'running' } };
if (operation === 'files') return { files: completed ? [{ name: 'story.txt', path: '/story.txt', directory_path: '/', is_dir: false, size: 1024 }] : [] };
return base(operation, input);
});
render(<CloudChat slug={slug} />);
await waitFor(() => expect(api.events).toHaveBeenCalledWith('run-1'));
await waitFor(() => expect(api.call).toHaveBeenCalledWith('files', { thread_id: 'thread-1', path: '/' }));
expect(screen.queryByRole('button', { name: /^附件/ })).not.toBeInTheDocument();
completed = true;
await act(async () => events.dispatchEvent(new MessageEvent('end', {
data: JSON.stringify({ run_id: 'run-1', thread_id: 'thread-1', payload: { chunk: { status: 'completed' } } }),
})));
expect(await screen.findByText('故事文件已做好')).toBeVisible();
fireEvent.click(await screen.findByRole('button', { name: '附件,共 1 个文件' }));
expect(await screen.findByRole('button', { name: '保存 story.txt' })).toBeEnabled();
expect(api.call.mock.calls.filter(([operation]) => operation === 'history')).toHaveLength(2);
expect(api.call.mock.calls.some(([operation]) => ['createThread', 'confirmAttachment', 'submit'].includes(operation))).toBe(false);
expect(api.upload).not.toHaveBeenCalled();
});