From d5415bb8928d5f4d68888ade0026c18a1ba80ff3 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:15:55 +0000 Subject: [PATCH] fix: show feedback for rejected prompt attachments --- .../ai-elements/prompt-input.test.tsx | 28 +++++++++++++++++++ .../components/ai-elements/prompt-input.tsx | 16 +++++++---- .../tasks/TaskPromptInput.client.test.tsx | 25 +++++++++++++++++ .../src/components/tasks/TaskPromptInput.tsx | 2 ++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/ai-elements/prompt-input.test.tsx b/apps/web/src/components/ai-elements/prompt-input.test.tsx index 1aafcdffd..838e54ece 100644 --- a/apps/web/src/components/ai-elements/prompt-input.test.tsx +++ b/apps/web/src/components/ai-elements/prompt-input.test.tsx @@ -151,6 +151,34 @@ describe('PromptInput', () => { expect(textarea).toHaveFocus(); }); + it('reports unsupported files in a mixed selection', () => { + const onError = vi.fn(); + const prompt = ( + {}} onError={onError}> + + + ); + const { container } = render(prompt); + + fireEvent.change(container.querySelector('input[type="file"]')!, { + target: { + files: [ + new File(['notes'], 'notes.txt', { type: 'text/plain' }), + new File(['unsupported'], 'attachment.bin', { + type: 'application/octet-stream', + }), + ], + }, + }); + + expect(screen.getByTestId('attachment-count')).toHaveTextContent('1'); + expect(onError).toHaveBeenCalledWith({ + code: 'accept', + message: + 'Some files were not added because their types are not supported.', + }); + }); + it('accepts attachments that match extension-based filters', () => { const { container } = render( {}}> diff --git a/apps/web/src/components/ai-elements/prompt-input.tsx b/apps/web/src/components/ai-elements/prompt-input.tsx index 3fa573a8b..a7d056bd3 100644 --- a/apps/web/src/components/ai-elements/prompt-input.tsx +++ b/apps/web/src/components/ai-elements/prompt-input.tsx @@ -347,13 +347,15 @@ export const PromptInput = ({ const incoming = Array.from(fileList); const accepted = incoming.filter((f) => matchesAccept(f)); - if (incoming.length && accepted.length === 0) { + if (incoming.length > accepted.length) { onError?.({ code: 'accept', - message: 'No files match the accepted types.', + message: accepted.length + ? 'Some files were not added because their types are not supported.' + : 'No files match the accepted types.', }); - return; + if (accepted.length === 0) return; } const withinSize = (f: File) => @@ -424,13 +426,15 @@ export const PromptInput = ({ const incoming = Array.from(fileList); const accepted = incoming.filter((f) => matchesAccept(f)); - if (incoming.length && accepted.length === 0) { + if (incoming.length > accepted.length) { onError?.({ code: 'accept', - message: 'No files match the accepted types.', + message: accepted.length + ? 'Some files were not added because their types are not supported.' + : 'No files match the accepted types.', }); - return; + if (accepted.length === 0) return; } const withinSize = (f: File) => diff --git a/apps/web/src/components/tasks/TaskPromptInput.client.test.tsx b/apps/web/src/components/tasks/TaskPromptInput.client.test.tsx index f84c92eac..2e8f267e5 100644 --- a/apps/web/src/components/tasks/TaskPromptInput.client.test.tsx +++ b/apps/web/src/components/tasks/TaskPromptInput.client.test.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { toast } from 'sonner'; import { createEvent, fireEvent, @@ -47,6 +48,30 @@ function KeyboardPrompt({ } describe('TaskPromptInput', () => { + it('reports rejected attachments without clearing the prompt', () => { + const errorToast = vi.spyOn(toast, 'error').mockReturnValue(0); + try { + const { container } = renderPromptInput(); + fireEvent.change(container.querySelector('input[type="file"]')!, { + target: { + files: [ + new File(['unsupported'], 'attachment.bin', { + type: 'application/octet-stream', + }), + ], + }, + }); + + expect(errorToast).toHaveBeenCalledWith( + 'No files match the accepted types.', + ); + expect(screen.getByRole('textbox')).toHaveValue('Fix the login bug'); + expect(screen.queryByText('attachment.bin')).not.toBeInTheDocument(); + } finally { + errorToast.mockRestore(); + } + }); + it('explains why the send button is disabled when hovering it', async () => { const reason = 'Create an environment before starting a task.'; diff --git a/apps/web/src/components/tasks/TaskPromptInput.tsx b/apps/web/src/components/tasks/TaskPromptInput.tsx index 02d14119e..bbfcc27ba 100644 --- a/apps/web/src/components/tasks/TaskPromptInput.tsx +++ b/apps/web/src/components/tasks/TaskPromptInput.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import { toast } from 'sonner'; import { BasicTooltip, SendHorizontal } from '@/components/system'; import { @@ -162,6 +163,7 @@ export function TaskPromptInput({ toast.error(error.message)} clearOnSubmit={false} accept={ROOMOTE_FILE_ATTACHMENT_ACCEPT} multiple