mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-18 05:59:42 +02:00
fix(ai-builder): Reactive subject name on chip (#39097)
This commit is contained in:
parent
7683e1c7f6
commit
233afff41c
|
|
@ -59,6 +59,7 @@ import {
|
|||
getDismissedContextKeys,
|
||||
handoffContextKey,
|
||||
} from '../instanceAi.handoffContext';
|
||||
import type { InstanceAiEmbedSubject } from '../embed/instanceAiEmbed.types';
|
||||
import InstanceAiMessage from './InstanceAiMessage.vue';
|
||||
import InstanceAiInput from './InstanceAiInput.vue';
|
||||
import InstanceAiMarkdown from './InstanceAiMarkdown.vue';
|
||||
|
|
@ -73,6 +74,14 @@ import CreditWarningBanner from '@/features/ai/assistant/components/Agent/Credit
|
|||
const props = defineProps<{
|
||||
/** Runs before every send (e.g. flush a pending autosave). Rejecting cancels the send. */
|
||||
beforeSend?: () => Promise<void>;
|
||||
/**
|
||||
* The live embed subject (an agent in the builder today). When it refers to
|
||||
* the same agent as the stashed `pendingAgentAttachment`, the chat-input
|
||||
* context chip follows this subject's `name` instead of the snapshot taken
|
||||
* at thread mint — so a rename in the host updates the chip live. Optional:
|
||||
* the full thread view passes no subject and keeps the stashed-name behavior.
|
||||
*/
|
||||
subject?: InstanceAiEmbedSubject;
|
||||
/** Extra scroll space for a panel that overlays messages above the input. */
|
||||
aboveInputOverlapHeight?: number;
|
||||
}>();
|
||||
|
|
@ -165,6 +174,13 @@ const hasFloatingConfirmation = computed(() =>
|
|||
const composerContextChip = computed(() => {
|
||||
const agentAttachment = currentAgentAttachment.value;
|
||||
if (agentAttachment && 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.
|
||||
const liveSubjectName =
|
||||
props.subject?.type === 'agent' && props.subject.id === agentAttachment.id
|
||||
? props.subject.name
|
||||
: undefined;
|
||||
return {
|
||||
type: 'agent-artifact' as const,
|
||||
agentId: agentAttachment.id,
|
||||
|
|
@ -173,7 +189,7 @@ const composerContextChip = computed(() => {
|
|||
pendingAgentAttachment.value?.id === agentAttachment.id &&
|
||||
pendingAgentAttachment.value.pending === true,
|
||||
key: `pending-agent:${agentAttachment.id}`,
|
||||
label: agentAttachment.name ?? i18n.baseText('agents.new.defaultName'),
|
||||
label: liveSubjectName ?? agentAttachment.name ?? i18n.baseText('agents.new.defaultName'),
|
||||
icon: 'robot',
|
||||
isPending: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { defineComponent, h, nextTick, reactive } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
stashPendingRedirectLanding,
|
||||
stashPendingWorkflowAttachment,
|
||||
} from '../../composables/useInstanceAiHandoff';
|
||||
import type { InstanceAiEmbedSubject } from '../../embed/instanceAiEmbed.types';
|
||||
import type { InstanceAiHandoffContext, InstanceAiMessage } from '@n8n/api-types';
|
||||
import { ResponseError } from '@n8n/rest-api-client';
|
||||
import { USER_TYPED_MESSAGE } from '../../prefills';
|
||||
|
|
@ -157,6 +158,82 @@ describe('InstanceAiConversation', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
describe('composer context chip label', () => {
|
||||
// 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).
|
||||
function mountWithSubject(subject: InstanceAiEmbedSubject | undefined) {
|
||||
thread.sseState = 'disconnected';
|
||||
stashPendingAgentAttachment('thread-1', {
|
||||
type: 'agent',
|
||||
id: 'agent-1',
|
||||
projectId: 'proj-1',
|
||||
name: 'Stashed Name',
|
||||
pending: true,
|
||||
});
|
||||
const reactiveSubject = subject === undefined ? undefined : reactive({ ...subject });
|
||||
const renderer = createThreadComponentRenderer(
|
||||
InstanceAiConversation,
|
||||
{
|
||||
props: { subject: reactiveSubject },
|
||||
global: { stubs: { InstanceAiInput: InstanceAiInputStub } },
|
||||
},
|
||||
() => thread,
|
||||
);
|
||||
return { ...renderer(), subject: reactiveSubject };
|
||||
}
|
||||
|
||||
it('prefers the live subject name over the stashed name when agent ids match', async () => {
|
||||
const { getByTestId } = mountWithSubject({
|
||||
type: 'agent',
|
||||
id: 'agent-1',
|
||||
projectId: 'proj-1',
|
||||
name: 'Renamed Live',
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(getByTestId('instance-ai-input-context-chip').textContent).toBe('Renamed Live'),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the chip when the live subject is renamed after mount', async () => {
|
||||
const { getByTestId, subject } = mountWithSubject({
|
||||
type: 'agent',
|
||||
id: 'agent-1',
|
||||
projectId: 'proj-1',
|
||||
name: 'Initial Name',
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(getByTestId('instance-ai-input-context-chip').textContent).toBe('Initial Name'),
|
||||
);
|
||||
|
||||
// A rename in the builder mutates the reactive subject's name; the chip
|
||||
// must follow it without a re-stash or remount.
|
||||
subject!.name = 'Renamed Mid-Session';
|
||||
await nextTick();
|
||||
expect(getByTestId('instance-ai-input-context-chip').textContent).toBe('Renamed Mid-Session');
|
||||
});
|
||||
|
||||
it('falls back to the stashed name when the subject refers to a different agent', async () => {
|
||||
const { getByTestId } = mountWithSubject({
|
||||
type: 'agent',
|
||||
id: 'agent-other',
|
||||
projectId: 'proj-1',
|
||||
name: 'Renamed Live',
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(getByTestId('instance-ai-input-context-chip').textContent).toBe('Stashed Name'),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the stashed name when no subject is provided', async () => {
|
||||
const { getByTestId } = mountWithSubject(undefined);
|
||||
await vi.waitFor(() =>
|
||||
expect(getByTestId('instance-ai-input-context-chip').textContent).toBe('Stashed Name'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('awaits beforeSend before sending, restoring the draft if it rejects', async () => {
|
||||
const beforeSend = vi.fn().mockRejectedValueOnce(new Error('flush failed'));
|
||||
const renderer = createThreadComponentRenderer(
|
||||
|
|
|
|||
|
|
@ -385,6 +385,9 @@ const ThreadScope = defineComponent({
|
|||
// defined inside the panel's own `<script setup>`, and this is the
|
||||
// only place that can reach the mounted conversation for `handoff()`.
|
||||
ref: conversationRef,
|
||||
// Forward the live subject so the chat-input context chip follows a
|
||||
// host rename instead of the snapshot stashed at thread mint.
|
||||
subject: props.subject,
|
||||
beforeSend: props.beforeSend,
|
||||
onThreadMissing: () => scopeEmit('thread-missing'),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user