mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): disable Draw direct-send during an active run, keep Queue (#3270)
Reinstates the Studio tool hardening from #3081 on top of current main: while a task is streaming, the Draw/annotation primary Send action and its Enter shortcut are disabled, so an annotation can no longer leak into the active run while the button shows a disabled reason. This is the synthesis of two stacked-merge-divergent changes rather than a wholesale revert: Queue stays available, so the value from #1961 (kami) is preserved — an annotation made during a run is still staged for the next turn instead of being dropped. Only the button/Enter availability changes; the downstream queue/streaming-staging handler in ChatComposer is untouched. - PreviewDrawOverlay: send('send') and canSend now respect sendDisabled. - Reframed the streaming Draw test to assert Send is disabled while Queue still emits a queued annotation (preserving the "annotate during a run" coverage). - Added unit coverage for the Enter/Send guard and Queue availability while a task is running.
This commit is contained in:
parent
912c7e380a
commit
bf7152dbdc
3 changed files with 72 additions and 5 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<PreviewDrawOverlay active sendDisabled sendDisabledReason="A task is currently running">
|
||||
<div style={{ width: 320, height: 200 }} />
|
||||
</PreviewDrawOverlay>,
|
||||
);
|
||||
|
||||
const input = container.querySelector<HTMLInputElement>('.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(
|
||||
<PreviewDrawOverlay active sendDisabled sendDisabledReason="A task is currently running">
|
||||
<div style={{ width: 320, height: 200 }} />
|
||||
</PreviewDrawOverlay>,
|
||||
);
|
||||
|
||||
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(
|
||||
<PreviewDrawOverlay active sendDisabled sendDisabledReason="A task is currently running">
|
||||
<div style={{ width: 320, height: 200 }} />
|
||||
</PreviewDrawOverlay>,
|
||||
);
|
||||
|
||||
const input = container.querySelector<HTMLInputElement>('.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(
|
||||
<PreviewDrawOverlay active>
|
||||
|
|
|
|||
Loading…
Reference in a new issue