From e79a2ba84bce39a65151d4ba7ad72da08266daeb Mon Sep 17 00:00:00 2001 From: Anne Aguirre Date: Fri, 2 Oct 2026 11:01:53 +0000 Subject: [PATCH] fix(editor): Remove the new-agent pill from the AI assistant composer (#40014) --- .../__tests__/InstanceAiInput.test.ts | 11 +---- .../__tests__/InstanceAiThreadView.test.ts | 16 +++--- .../createThreadComponentRenderer.ts | 6 +++ .../components/InstanceAiConversation.vue | 19 +++++-- .../instanceAi/components/InstanceAiInput.vue | 3 -- .../__tests__/InstanceAiConversation.test.ts | 49 ++++++++++++++++++- .../ai/instanceAi/instanceAi.contextChip.ts | 1 - 7 files changed, 78 insertions(+), 27 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts index 8d58fff2787..d95911269e4 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts @@ -314,16 +314,10 @@ describe('InstanceAiInput', () => { ); }); - it('uses the new agent placeholder for a pending agent artifact', () => { + it('uses the new agent placeholder when the caller passes its key', () => { const { getByRole } = renderComponent({ props: { - contextChip: { - type: 'agent-artifact', - agentId: 'agent-1', - projectId: 'project-1', - isNewAgent: true, - label: 'New Agent', - }, + placeholderKey: 'instanceAi.input.newAgentPlaceholder', }, }); @@ -340,7 +334,6 @@ describe('InstanceAiInput', () => { type: 'agent-artifact', agentId: 'agent-1', projectId: 'project-1', - isNewAgent: false, label: 'Support Agent', }, }, diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts index bf60287d148..1bfb2adc1e9 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts @@ -1394,7 +1394,7 @@ describe('InstanceAiThreadView', () => { }); }); - it('shows the new-agent context until the first successful message submission', async () => { + it('attaches the new-agent context until the first successful message submission', async () => { thread.sseState = 'disconnected'; vi.mocked(thread.loadHistoricalMessages).mockResolvedValue('skipped'); store.threads = [ @@ -1435,7 +1435,8 @@ describe('InstanceAiThreadView', () => { expect(preview).toHaveAttribute('data-agent-id', 'agent-1'); expect(preview).toHaveAttribute('data-project-id', 'project-1'); expect(thread.sendMessage).not.toHaveBeenCalled(); - expect(getByTestId('instance-ai-input-context-chip')).toHaveTextContent('New Agent'); + // A brand-new agent shows no composer chip — the attachment still rides. + expect(getByTestId('instance-ai-input-context-chip')).toHaveTextContent(''); await userEvent.click(getByTestId('instance-ai-input-submit')); @@ -1454,7 +1455,7 @@ describe('InstanceAiThreadView', () => { handoffContext: undefined, }); expect(getPendingAgentAttachment('thread-1')).not.toBeNull(); - expect(getByTestId('instance-ai-input-context-chip')).toHaveTextContent('New Agent'); + expect(getByTestId('instance-ai-input-context-chip')).toHaveTextContent(''); await userEvent.click(getByTestId('instance-ai-input-submit')); @@ -1510,11 +1511,12 @@ describe('InstanceAiThreadView', () => { pending: true, }); - const { getByTestId } = renderView({ props: { threadId: 'thread-1' } }); + const { findByTestId, getByTestId } = renderView({ props: { threadId: 'thread-1' } }); - await vi.waitFor(() => { - expect(getByTestId('instance-ai-input-context-chip')).toHaveTextContent('Support Agent'); - }); + // The stash is still the pending new-agent one, so no chip — but the + // attachment follows the bound target's persisted name. + await findByTestId('instance-ai-agent-preview-stub'); + expect(getByTestId('instance-ai-input-context-chip')).toHaveTextContent(''); await userEvent.click(getByTestId('instance-ai-input-submit')); expect(thread.sendMessage).toHaveBeenCalledWith('Normal message', { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/createThreadComponentRenderer.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/createThreadComponentRenderer.ts index 0ba88418e10..18ca3033591 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/createThreadComponentRenderer.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/createThreadComponentRenderer.ts @@ -115,6 +115,7 @@ export const InstanceAiInputStub = defineComponent({ isSubmitting: { type: Boolean, required: false }, isWorkflowBuilderAvailable: { type: Boolean, required: false }, contextChip: { type: Object, required: false }, + placeholderKey: { type: String, required: false }, mentionsEnabled: { type: Boolean, required: false }, mentionProjectId: { type: String, required: false }, mentionArtifacts: { type: Array, required: false }, @@ -187,6 +188,11 @@ export const InstanceAiInputStub = defineComponent({ { 'data-test-id': 'instance-ai-input-context-chip-icon' }, props.contextChip?.icon ?? '', ), + h( + 'span', + { 'data-test-id': 'instance-ai-input-placeholder-key' }, + props.placeholderKey ?? 'unset', + ), h('span', { 'data-test-id': 'instance-ai-input-draft' }, inputDraft.value), h( 'span', diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiConversation.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiConversation.vue index 54f7fffbd1c..3d243ea7397 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiConversation.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiConversation.vue @@ -297,7 +297,12 @@ const awaitingOnboardingGreeting = computed( const composerContextChip = computed(() => { const agentAttachment = currentAgentAttachment.value; - if (agentAttachment && pendingComposerContext.value?.source !== 'agent-preview') { + const isNewAgent = + agentAttachment !== null && + pendingAgentAttachment.value?.id === agentAttachment.id && + pendingAgentAttachment.value.pending === true; + // A brand-new agent is already the focus of the builder — no composer chip. + if (agentAttachment && !isNewAgent && pendingComposerContext.value?.source !== 'agent-preview') { // Prefer the host's live subject name when it refers to the same agent as // the stashed attachment, so a rename in the builder updates the chip // without re-stashing. Falls back to the stashed snapshot otherwise. @@ -309,9 +314,6 @@ const composerContextChip = computed(() => { type: 'agent-artifact' as const, agentId: agentAttachment.id, projectId: agentAttachment.projectId, - isNewAgent: - pendingAgentAttachment.value?.id === agentAttachment.id && - pendingAgentAttachment.value.pending === true, key: `pending-agent:${agentAttachment.id}`, label: liveSubjectName ?? agentAttachment.name ?? i18n.baseText('agents.new.defaultName'), icon: 'robot', @@ -375,6 +377,10 @@ const composerContextChip = computed(() => { return null; }); +// The new-agent placeholder no longer rides the context chip (a pending agent +// shows none), so it is passed to the input as an explicit placeholder key. +const isPendingAgentComposer = computed(() => pendingAgentAttachment.value?.pending === true); + const workflowHandoffGreeting = computed(() => { const attachment = thread.pendingWorkflowAttachment; if (!attachment || thread.isHydratingThread || thread.hasMessages) return null; @@ -821,7 +827,7 @@ function dismissPendingComposerContext(key: string): boolean { async function dismissComposerContextChip() { if (!composerContextChip.value) return; - if (pendingAgentAttachment.value && pendingComposerContext.value?.source !== 'agent-preview') { + if (composerContextChip.value.type === 'agent-artifact' && pendingAgentAttachment.value) { clearPendingAgentAttachment(thread.id); pendingAgentAttachment.value = null; return; @@ -1030,6 +1036,9 @@ defineExpose({ :current-thread-id="thread.id" :amend-context="thread.amendContext" :context-chip="composerContextChip" + :placeholder-key=" + isPendingAgentComposer ? 'instanceAi.input.newAgentPlaceholder' : undefined + " :contextual-suggestion="thread.contextualSuggestion" :mentions-enabled="props.mentionsEnabled" :mention-project-id="thread.projectId" diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue index bb252235c45..24ff3098943 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue @@ -451,9 +451,6 @@ const placeholder = computed(() => { if (props.contextualSuggestion) { return props.contextualSuggestion; } - if (props.contextChip?.type === 'agent-artifact' && props.contextChip.isNewAgent) { - return i18n.baseText('instanceAi.input.newAgentPlaceholder'); - } return i18n.baseText(props.placeholderKey ?? 'instanceAi.input.placeholder'); }); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/__tests__/InstanceAiConversation.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/__tests__/InstanceAiConversation.test.ts index 7dc766f325e..c9fe305a9f8 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/__tests__/InstanceAiConversation.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/__tests__/InstanceAiConversation.test.ts @@ -270,7 +270,8 @@ describe('InstanceAiConversation', () => { // Wraps the subject in `reactive` and returns it alongside the render // result, so a test can mutate `subject.name` after mount and assert the // chip follows the live value — the actual AGENT-954 scenario (a rename in - // the builder while the panel stays open). + // the builder while the panel stays open). The stashed agent is a saved + // one: a pending (brand-new) agent shows no chip at all. function mountWithSubject(subject: InstanceAiEmbedSubject | undefined) { thread.sseState = 'disconnected'; stashPendingAgentAttachment('thread-1', { @@ -278,7 +279,6 @@ describe('InstanceAiConversation', () => { id: 'agent-1', projectId: 'proj-1', name: 'Stashed Name', - pending: true, }); const reactiveSubject = subject === undefined ? undefined : reactive({ ...subject }); const renderer = createThreadComponentRenderer( @@ -342,6 +342,51 @@ describe('InstanceAiConversation', () => { }); }); + describe('pending agent composer', () => { + function mountWithPendingAgent() { + thread.sseState = 'disconnected'; + stashPendingAgentAttachment('thread-1', { + type: 'agent', + id: 'agent-1', + projectId: 'proj-1', + pending: true, + }); + const renderer = createThreadComponentRenderer( + InstanceAiConversation, + { + global: { stubs: { InstanceAiInput: InstanceAiInputStub } }, + }, + () => thread, + ); + return renderer(); + } + + it('shows no context chip, but keeps the new-agent placeholder', async () => { + const { getByTestId } = mountWithPendingAgent(); + + await vi.waitFor(() => + expect(getByTestId('instance-ai-input-placeholder-key').textContent).toBe( + 'instanceAi.input.newAgentPlaceholder', + ), + ); + expect(getByTestId('instance-ai-input-context-chip').textContent).toBe(''); + }); + + it('still attaches the pending agent to the first message', async () => { + const { getByTestId } = mountWithPendingAgent(); + + await fireEvent.click(getByTestId('instance-ai-input-submit')); + await vi.waitFor(() => expect(thread.sendMessage).toHaveBeenCalled()); + + expect(thread.sendMessage).toHaveBeenCalledWith( + 'Normal message', + expect.objectContaining({ + attachments: [{ type: 'agent', id: 'agent-1', projectId: 'proj-1', pending: true }], + }), + ); + }); + }); + it('awaits beforeSend before sending, restoring the draft if it rejects', async () => { const beforeSend = vi.fn().mockRejectedValueOnce(new Error('flush failed')); const renderer = createThreadComponentRenderer( diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.contextChip.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.contextChip.ts index 32b5449f428..c3a221a3148 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.contextChip.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.contextChip.ts @@ -8,7 +8,6 @@ export type AgentArtifactContextChip = ContextChipBase & { type: 'agent-artifact'; agentId: string; projectId: string; - isNewAgent: boolean; }; export type AgentPreviewSessionContextChip = ContextChipBase & {