diff --git a/apps/web/src/components/PreviewDrawOverlay.tsx b/apps/web/src/components/PreviewDrawOverlay.tsx index 9c8955faa8..7d3750cb6d 100644 --- a/apps/web/src/components/PreviewDrawOverlay.tsx +++ b/apps/web/src/components/PreviewDrawOverlay.tsx @@ -364,6 +364,9 @@ export function PreviewDrawOverlay({ const shouldCapture = hasInk || hasTarget || captureViewport; const canSubmit = shouldCapture || Boolean(note.trim()); if (sending || !canSubmit) return; + // While a task is running the primary Send is disabled (use Queue instead). + // The note/attachment is not lost: Queue still stages it for the next turn. + if (action === 'send' && sendDisabled) return; setCaptureWarning(null); setPendingAction(action); try { @@ -425,7 +428,7 @@ export function PreviewDrawOverlay({ const overlayPointer = active ? 'auto' : 'none'; const showCanvas = active || hasInk; const canSubmit = hasInk || Boolean(captureTarget) || captureViewport || Boolean(note.trim()); - const canSend = canSubmit; + const canSend = canSubmit && !sendDisabled; const canUndo = undoCount > 0 && !sending; const canRedo = redoCount > 0 && !sending; diff --git a/apps/web/tests/components/FileViewer.test.tsx b/apps/web/tests/components/FileViewer.test.tsx index 76f5856e29..d520c99cd6 100644 --- a/apps/web/tests/components/FileViewer.test.tsx +++ b/apps/web/tests/components/FileViewer.test.tsx @@ -1722,7 +1722,7 @@ describe('FileViewer tweaks toolbar', () => { expect((screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement).srcdoc).toBe(frame.srcdoc); }); - it('lets Draw direct send emit a queued annotation while a task is running', async () => { + it('disables Draw direct send during a run but keeps Queue available so the annotation is not lost', async () => { const annotationSpy = vi.fn(); window.addEventListener(ANNOTATION_EVENT, annotationSpy); @@ -1738,17 +1738,22 @@ describe('FileViewer tweaks toolbar', () => { target: { value: 'mark this' }, }); + // While a task is running the primary Send is disabled; Queue stays available + // so the annotation is staged for the next turn rather than sent mid-run. + const send = screen.getByRole('button', { name: 'Send' }) as HTMLButtonElement; + expect(send.disabled).toBe(true); const queue = screen.getByRole('button', { name: 'Queue' }) as HTMLButtonElement; expect(queue.disabled).toBe(false); - const send = screen.getByRole('button', { name: 'Send' }) as HTMLButtonElement; - expect(send.disabled).toBe(false); fireEvent.click(send); + expect(annotationSpy).not.toHaveBeenCalled(); + + fireEvent.click(queue); await waitFor(() => expect(annotationSpy).toHaveBeenCalledTimes(1)); expect(annotationSpy.mock.calls[0]?.[0]).toMatchObject({ detail: { - action: 'send', + action: 'queue', note: 'mark this', filePath: 'preview.html', }, diff --git a/apps/web/tests/components/PreviewDrawOverlay.test.tsx b/apps/web/tests/components/PreviewDrawOverlay.test.tsx index 6152ddf3b8..61c1f14526 100644 --- a/apps/web/tests/components/PreviewDrawOverlay.test.tsx +++ b/apps/web/tests/components/PreviewDrawOverlay.test.tsx @@ -61,6 +61,65 @@ describe('PreviewDrawOverlay', () => { } }); + it('does not direct-send via Enter while a task is running', () => { + const annotation = vi.fn(); + window.addEventListener('opendesign:annotation', annotation); + + try { + const { container } = render( + +
+ , + ); + + const input = container.querySelector('.preview-draw-note-input'); + expect(input).toBeTruthy(); + + fireEvent.change(input!, { target: { value: 'Please inspect this panel.' } }); + fireEvent.keyDown(input!, { key: 'Enter' }); + + expect(annotation).not.toHaveBeenCalled(); + } finally { + window.removeEventListener('opendesign:annotation', annotation); + } + }); + + it('disables the primary Send action while a task is running', () => { + const { getByRole } = render( + +
+ , + ); + + const sendButton = getByRole('button', { name: 'Send' }); + expect((sendButton as HTMLButtonElement).disabled).toBe(true); + }); + + it('keeps Queue available so an annotation is not lost while a task is running', async () => { + const annotation = vi.fn(); + window.addEventListener('opendesign:annotation', annotation); + + try { + const { container, getByRole } = render( + +
+ , + ); + + const input = container.querySelector('.preview-draw-note-input'); + fireEvent.change(input!, { target: { value: 'Queue this up.' } }); + + const queueButton = getByRole('button', { name: 'Queue' }); + expect((queueButton as HTMLButtonElement).disabled).toBe(false); + fireEvent.click(queueButton); + + await waitFor(() => expect(annotation).toHaveBeenCalledTimes(1)); + expect(annotation.mock.calls[0]?.[0].detail).toMatchObject({ action: 'queue' }); + } finally { + window.removeEventListener('opendesign:annotation', annotation); + } + }); + it('clears transient ink when draw mode exits', async () => { const { container, rerender } = render(