From 3c741a4a0b6cfd975adaa19069e16235edd84450 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Fri, 9 Oct 2026 14:21:18 +0200 Subject: [PATCH] refactor(editor): remove held message controls from plan stops --- .../frontend/@n8n/i18n/src/locales/en.json | 7 +- .../agents/__tests__/AgentChatPanel.test.ts | 44 -------- .../useAgentTaskCancellation.test.ts | 8 +- .../features/agents/agents-design-language.md | 1 + .../agents/components/AgentChatPanel.vue | 104 +----------------- .../agents/composables/useAgentApi.ts | 13 --- .../agents/composables/useAgentChatStream.ts | 27 ----- .../ai/shared/components/ChatMessageQueue.vue | 98 ++++++++--------- 8 files changed, 54 insertions(+), 248 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 900173c0e04..8153de1ef04 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -8420,12 +8420,9 @@ "agents.chat.tasks.stopping": "Stopping tasks…", "agents.chat.tasks.reportFailed": "Tasks stopped. The agent could not write a summary. You can ask to continue.", "agents.chat.tasks.stopped": "Stopped", - "agents.chat.tasks.retry": "Stop all tasks again", - "agents.chat.tasks.stoppedNotice": "Tasks stopped. Completed: {completed}. Canceled: {canceled}. Your queued messages stay held until you select Send.", + "agents.chat.tasks.retry": "Retry stopping", "agents.chat.tasks.stopFailedTitle": "Some tasks still need to stop", - "agents.chat.tasks.stopFailed": "These tasks still need to stop. Select Stop all tasks again:", - "agents.chat.queue.send": "Send", - "agents.chat.queue.sendError": "Could not send the held message", + "agents.chat.queue.sendError": "Couldn’t send the message. Try again.", "agents.chat.plan.title": "Plan: {title}", "agents.chat.plan.progress": "{done} of {total} task done | {done} of {total} tasks done", "agents.chat.plan.viewTrace": "View trace", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentChatPanel.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentChatPanel.test.ts index 7dc09c5c212..b30564eb5ef 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentChatPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentChatPanel.test.ts @@ -88,8 +88,6 @@ vi.mock('../composables/useAgentTaskCancellation', () => ({ })); const isStoppingMock = ref(false); const showErrorMock = vi.fn(); -const sendHeldMessageMock = vi.fn(); -const updateQueuedMessageMock = vi.fn().mockResolvedValue('updated'); vi.mock('../composables/useAgentBackgroundJobs', () => ({ useAgentBackgroundJobs: () => ({ jobs: backgroundJobsMock, @@ -127,7 +125,6 @@ vi.mock('@n8n/i18n', () => { 'agents.chat.tasks.stopping': 'Stopping tasks…', 'agents.chat.tasks.stopped': 'Stopped', 'agents.chat.tasks.retry': 'Retry stopping', - 'agents.chat.queue.send': 'Send', 'agents.chat.input.placeholder.withAgent': `Message ${options?.interpolate?.agentName}…`, 'chat.messageQueue.title': `${options?.interpolate?.count} ${options?.adjustToNumber === 1 ? 'message' : 'messages'} up next`, 'agents.chat.attachments.unsupportedType': `${options?.interpolate?.fileName} is not a supported file type`, @@ -339,8 +336,6 @@ vi.mock('../composables/useAgentChatStream', () => ({ steeringQueueIds: steeringQueueIdsMock, canSteer: canSteerMock, steerQueuedMessage: steerQueuedMessageMock, - sendHeldMessage: sendHeldMessageMock, - updateQueuedMessage: updateQueuedMessageMock, removeQueuedMessage: removeQueuedMessageMock, reorderQueuedMessage: reorderQueuedMessageMock, isReorderingQueue: isReorderingQueueMock, @@ -1128,41 +1123,6 @@ describe('AgentChatPanel', () => { wrapper.unmount(); }); - it('sends only the selected held message and keeps the other rows editable', async () => { - queuedMessagesMock.value = [ - { id: 'held-1', message: 'First', held: true, steeringExecutionId: null, createdAt: '' }, - { id: 'held-2', message: 'Second', held: true, steeringExecutionId: null, createdAt: '' }, - ]; - const wrapper = mountPanel(); - const rows = wrapper.findAll('[data-testid="chat-queued-message"]'); - const send = rows[0].findAll('button').find((button) => button.text() === 'Send'); - expect(send).toBeDefined(); - await send!.trigger('click'); - expect(sendHeldMessageMock).toHaveBeenCalledExactlyOnceWith('held-1'); - expect(steerQueuedMessageMock).not.toHaveBeenCalled(); - expect(rows[1].find('[aria-label="generic.edit"]').exists()).toBe(true); - expect(rows[1].find('[aria-label="generic.delete"]').exists()).toBe(true); - wrapper.unmount(); - }); - - it('saves edits to a held message without removing or sending it', async () => { - queuedMessagesMock.value = [ - { id: 'held-1', message: 'Original', held: true, steeringExecutionId: null, createdAt: '' }, - ]; - const wrapper = mountPanel(); - await wrapper - .get('[data-testid="chat-queued-message"] [aria-label="generic.edit"]') - .trigger('click'); - const editor = wrapper.get('textarea[aria-label="agents.chat.queue.edit"]'); - await editor.setValue('Changed'); - await editor.trigger('keydown', { key: 'Enter' }); - await flushPromises(); - expect(updateQueuedMessageMock).toHaveBeenCalledWith('held-1', 'Changed'); - expect(removeQueuedMessageMock).not.toHaveBeenCalled(); - expect(sendHeldMessageMock).not.toHaveBeenCalled(); - wrapper.unmount(); - }); - it('restores composer focus when a plan stop finishes after the request returns', async () => { backgroundJobsMock.value = [ { id: 'job-1', kind: 'subagent', title: 'Research', status: 'running', startedAt: '' }, @@ -1190,9 +1150,7 @@ describe('AgentChatPanel', () => { status: 'stopped', requestedAt: new Date().toISOString(), failures: [], - summary: { completed: 0, canceled: 0 }, plan: null, - heldQueueIds: [], }; isStoppingMock.value = false; await flushPromises(); @@ -1646,9 +1604,7 @@ describe('AgentChatPanel', () => { status: 'stopping', requestedAt: new Date().toISOString(), failures: [], - summary: { completed: 0, canceled: 0 }, plan: null, - heldQueueIds: [], }; await nextTick(); expect(card.props('stopping')).toBe(true); diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentTaskCancellation.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentTaskCancellation.test.ts index 671e5856b4a..41ff3fd82d3 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentTaskCancellation.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentTaskCancellation.test.ts @@ -25,9 +25,7 @@ const stopped: AgentTaskCancellationState = { status: 'stopped', requestedAt: '2026-10-01T10:00:00.000Z', failures: [], - summary: { completed: 1, canceled: 2 }, plan: null, - heldQueueIds: ['1'], }; const event: PushMessage = { type: 'agentBackgroundTasksUpdated', @@ -93,11 +91,11 @@ it('restores cancellation from server state and refreshes on push', async () => vi.mocked(getAgentTaskCancellation).mockResolvedValue(stopped); const state = create(); await flushPromises(); - expect(state.state.value?.heldQueueIds).toEqual(['1']); - vi.mocked(getAgentTaskCancellation).mockResolvedValue({ ...stopped, heldQueueIds: [] }); + expect(state.state.value?.status).toBe('stopped'); + vi.mocked(getAgentTaskCancellation).mockResolvedValue({ ...stopped, status: 'resumed' }); onEvent(event); await flushPromises(); - expect(state.state.value?.heldQueueIds).toEqual([]); + expect(state.state.value?.status).toBe('resumed'); }); it('does not apply an old cancellation response to another conversation', async () => { diff --git a/packages/frontend/editor-ui/src/features/agents/agents-design-language.md b/packages/frontend/editor-ui/src/features/agents/agents-design-language.md index 69379829a79..befda5a338b 100644 --- a/packages/frontend/editor-ui/src/features/agents/agents-design-language.md +++ b/packages/frontend/editor-ui/src/features/agents/agents-design-language.md @@ -318,6 +318,7 @@ for this reply. Hide empty results entries in saved chats. Disable tools for thi reply. Keep Continue available. Composer Stop controls only the current response, including the acknowledgement. If a stop request fails, keep the action available for a retry. Do not show **Stopped** while the plan work is active. +Discard queued messages when the user stops the plan. ## Extend this document diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentChatPanel.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentChatPanel.vue index 84c0f5c63f4..8c5d2b8bc8e 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentChatPanel.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentChatPanel.vue @@ -18,7 +18,6 @@ import { N8nCallout, N8nIcon, N8nIconButton, - N8nInput, N8nLink, N8nText, N8nTooltip, @@ -234,8 +233,6 @@ const { steeringQueueIds, canSteer, steerQueuedMessage, - sendHeldMessage, - updateQueuedMessage, removeQueuedMessage, reorderQueuedMessage, isReorderingQueue, @@ -338,22 +335,6 @@ const showPlanStop = computed( ); const editingQueueId = ref(); -const heldQueueEdit = ref<{ id: string; text: string; saving: boolean }>(); -function isHeldQueueItem(id: string) { - return queuedMessages.value.some((item) => item.id === id && item.held); -} -async function saveHeldQueueEdit() { - const edit = heldQueueEdit.value; - if (!edit || edit.saving) return; - edit.saving = true; - try { - const result = await updateQueuedMessage(edit.id, edit.text); - if (heldQueueEdit.value === edit && result === 'updated') heldQueueEdit.value = undefined; - } finally { - edit.saving = false; - } -} - provide(AGENT_ATTACHMENT_URL_KEY, (attachmentId) => getChatAttachmentUrl( rootStore.restApiContext, @@ -420,11 +401,6 @@ async function loadChatAttachmentFile(attachment: ChatMessageAttachment): Promis } async function startQueueEdit(item: AgentChatQueueItem) { - if (item.held) { - if (isQueueItemBusy(item)) return; - heldQueueEdit.value = { id: item.id, text: item.message, saving: false }; - return; - } if (hasDraft.value || isQueueItemBusy(item) || isSubmissionBlocked.value) return; queueExpanded.value = true; const target = { @@ -632,22 +608,6 @@ watch( }, ); -async function sendHeldQueueMessage(queueId: string, event: MouseEvent) { - const button = event.currentTarget; - const restore = button === document.activeElement; - const threadId = props.continueSessionId; - await sendHeldMessage(queueId); - await nextTick(); - if ( - restore && - !disposed && - props.visible && - props.continueSessionId === threadId && - (document.activeElement === document.body || document.activeElement === button) - ) - focusInput({ preventScroll: true }); -} - let taskStopFocus: | { button: EventTarget | null; threadId?: string; projectId: string; agentId: string } | undefined; @@ -1279,7 +1239,6 @@ watch( queuedExternalMessage = undefined; handoffPreview = undefined; editingQueueId.value = undefined; - heldQueueEdit.value = undefined; queueExpanded.value = false; queueOrder.value = undefined; firstMessagePreview.value = undefined; @@ -1894,68 +1853,7 @@ onBeforeUnmount(() => { @steer="steerQueuedMessage" @edit="editQueuedMessage" @remove="removeQueuedMessage" - > - - - + />