fix(editor): Remove the new-agent pill from the AI assistant composer (#40014)

This commit is contained in:
Anne Aguirre 2026-10-02 11:01:53 +00:00 committed by GitHub
parent f6f71abef4
commit e79a2ba84b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 78 additions and 27 deletions

View File

@ -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',
},
},

View File

@ -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', {

View File

@ -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',

View File

@ -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"

View File

@ -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');
});

View File

@ -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(

View File

@ -8,7 +8,6 @@ export type AgentArtifactContextChip = ContextChipBase & {
type: 'agent-artifact';
agentId: string;
projectId: string;
isNewAgent: boolean;
};
export type AgentPreviewSessionContextChip = ContextChipBase & {