From 7fff6db9f9c26cdbc4a9f8a52fb4643f7e048e6c Mon Sep 17 00:00:00 2001 From: autologie Date: Wed, 10 Dec 2025 09:17:07 +0100 Subject: [PATCH 01/32] feat: add icon to personal and workflow agent --- packages/@n8n/api-types/src/chat-hub.ts | 29 +++- packages/@n8n/api-types/src/index.ts | 2 + .../1765361177092-AddIconToAgentTable.ts | 25 +++ .../@n8n/db/src/migrations/mysqldb/index.ts | 2 + .../db/src/migrations/postgresdb/index.ts | 2 + .../@n8n/db/src/migrations/sqlite/index.ts | 2 + .../modules/chat-hub/chat-hub-agent.entity.ts | 8 +- .../chat-hub/chat-hub-agent.service.ts | 34 ++-- .../chat-hub/chat-hub-session.entity.ts | 9 +- .../chat-hub/chat-hub-workflow.service.ts | 122 +++++++++++++- .../chat-hub/chat-hub.models.service.ts | 85 +--------- .../src/modules/chat-hub/chat-hub.service.ts | 59 ++++--- .../src/modules/chat-hub/chat-hub.types.ts | 1 - .../NavigationDropdown.vue | 77 ++++++++- .../frontend/@n8n/i18n/src/locales/en.json | 25 +-- ...ntsView.vue => ChatPersonalAgentsView.vue} | 131 +++------------ .../src/features/ai/chatHub/ChatView.vue | 26 +-- .../ai/chatHub/ChatWorkflowAgentsView.vue | 153 ++++++++++++++++++ .../src/features/ai/chatHub/chat.store.ts | 54 +++---- .../src/features/ai/chatHub/chat.types.ts | 14 +- .../src/features/ai/chatHub/chat.utils.ts | 14 +- .../chatHub/components/AgentEditorModal.vue | 58 +++++-- .../ai/chatHub/components/ChatAgentAvatar.vue | 46 ++++-- .../ai/chatHub/components/ChatAgentCard.vue | 28 ++-- .../components/ChatAgentSearchSort.vue | 87 ++++++++++ .../ai/chatHub/components/ChatMessage.vue | 6 +- .../components/ChatSessionMenuItem.vue | 2 +- .../chatHub/components/ChatSidebarContent.vue | 32 +++- .../ai/chatHub/components/ModelSelector.vue | 73 ++++++++- .../src/features/ai/chatHub/constants.ts | 3 +- .../features/ai/chatHub/module.descriptor.ts | 30 +++- 31 files changed, 876 insertions(+), 363 deletions(-) create mode 100644 packages/@n8n/db/src/migrations/common/1765361177092-AddIconToAgentTable.ts rename packages/frontend/editor-ui/src/features/ai/chatHub/{ChatAgentsView.vue => ChatPersonalAgentsView.vue} (61%) create mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue create mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatAgentSearchSort.vue diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 5d22a39f3eac3..2384fe08a2aa1 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -28,6 +28,21 @@ export const chatHubLLMProviderSchema = z.enum([ ]); export type ChatHubLLMProvider = z.infer; +/** + * Schema for icon or emoji representation + */ +export const agentIconOrEmojiSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('icon'), + value: z.string(), + }), + z.object({ + type: z.literal('emoji'), + value: z.string(), + }), +]); +export type AgentIconOrEmoji = z.infer; + export const chatHubProviderSchema = z.enum([ ...chatHubLLMProviderSchema.options, 'n8n', @@ -221,8 +236,10 @@ export interface ChatModelDto { model: ChatHubConversationModel; name: string; description: string | null; + icon: AgentIconOrEmoji | null; updatedAt: string | null; createdAt: string | null; + projectName: string | null; metadata: ChatModelMetadataDto; } @@ -305,7 +322,6 @@ export class ChatHubSendMessageRequest extends Z.class({ ), tools: z.array(INodeSchema), attachments: z.array(chatAttachmentSchema), - agentName: z.string().optional(), timeZone: TimeZoneSchema, }) {} @@ -336,12 +352,7 @@ export class ChatHubEditMessageRequest extends Z.class({ export class ChatHubUpdateConversationRequest extends Z.class({ title: z.string().optional(), credentialId: z.string().max(36).optional(), - agent: z - .object({ - model: chatHubConversationModelSchema, - name: z.string(), - }) - .optional(), + model: chatHubConversationModelSchema.optional(), tools: z.array(INodeSchema).optional(), }) {} @@ -362,6 +373,7 @@ export interface ChatHubSessionDto { workflowId: string | null; agentId: string | null; agentName: string; + agentIcon: AgentIconOrEmoji | null; createdAt: string; updatedAt: string; tools: INode[]; @@ -413,6 +425,7 @@ export interface ChatHubAgentDto { id: string; name: string; description: string | null; + icon: AgentIconOrEmoji; systemPrompt: string; ownerId: string; credentialId: string | null; @@ -426,6 +439,7 @@ export interface ChatHubAgentDto { export class ChatHubCreateAgentRequest extends Z.class({ name: z.string().min(1).max(128), description: z.string().max(512).optional(), + icon: agentIconOrEmojiSchema, systemPrompt: z.string().min(1), credentialId: z.string(), provider: chatHubLLMProviderSchema, @@ -436,6 +450,7 @@ export class ChatHubCreateAgentRequest extends Z.class({ export class ChatHubUpdateAgentRequest extends Z.class({ name: z.string().min(1).max(128).optional(), description: z.string().max(512).optional(), + icon: agentIconOrEmojiSchema.optional(), systemPrompt: z.string().min(1).optional(), credentialId: z.string().optional(), provider: chatHubProviderSchema.optional(), diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index 23ecf0421476b..0da5177f14d19 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -46,6 +46,8 @@ export { type ChatHubAgentDto, ChatHubCreateAgentRequest, ChatHubUpdateAgentRequest, + type AgentIconOrEmoji, + agentIconOrEmojiSchema, type EnrichedStructuredChunk, type ChatHubAgentTool, UpdateChatSettingsRequest, diff --git a/packages/@n8n/db/src/migrations/common/1765361177092-AddIconToAgentTable.ts b/packages/@n8n/db/src/migrations/common/1765361177092-AddIconToAgentTable.ts new file mode 100644 index 0000000000000..e95e57ac8768a --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1765361177092-AddIconToAgentTable.ts @@ -0,0 +1,25 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +const agentsTableName = 'chat_hub_agents'; +const sessionsTableName = 'chat_hub_sessions'; +const defaultIcon = JSON.stringify({ type: 'icon', value: 'bot' }); + +export class AddIconToAgentTable1765361177092 implements ReversibleMigration { + async up({ schemaBuilder: { addColumns, column }, queryRunner, escape }: MigrationContext) { + // Add icon column to agents table + await addColumns(agentsTableName, [column('icon').json]); + + // Update all existing agents with default icon + await queryRunner.query( + `UPDATE ${escape.tableName(agentsTableName)} SET ${escape.columnName('icon')} = '${defaultIcon}' WHERE ${escape.columnName('icon')} IS NULL`, + ); + + // Add agentIcon column to sessions table (nullable, no default update needed) + await addColumns(sessionsTableName, [column('agentIcon').json]); + } + + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns(agentsTableName, ['icon']); + await dropColumns(sessionsTableName, ['agentIcon']); + } +} diff --git a/packages/@n8n/db/src/migrations/mysqldb/index.ts b/packages/@n8n/db/src/migrations/mysqldb/index.ts index 80189087b88ef..e04424be67e72 100644 --- a/packages/@n8n/db/src/migrations/mysqldb/index.ts +++ b/packages/@n8n/db/src/migrations/mysqldb/index.ts @@ -124,6 +124,7 @@ import { AddCreatorIdToProjectTable1764276827837 } from '../common/1764276827837 import { CreateDynamicCredentialResolverTable1764682447000 } from '../common/1764682447000-CreateCredentialResolverTable'; import { AddDynamicCredentialEntryTable1764689388394 } from '../common/1764689388394-AddDynamicCredentialEntryTable'; import { BackfillMissingWorkflowHistoryRecords1765448186933 } from '../common/1765448186933-BackfillMissingWorkflowHistoryRecords'; +import { AddIconToAgentTable1765361177092 } from '../common/1765361177092-AddIconToAgentTable'; import type { Migration } from '../migration-types'; export const mysqlMigrations: Migration[] = [ @@ -253,4 +254,5 @@ export const mysqlMigrations: Migration[] = [ CreateDynamicCredentialResolverTable1764682447000, AddDynamicCredentialEntryTable1764689388394, BackfillMissingWorkflowHistoryRecords1765448186933, + AddIconToAgentTable1765361177092, ]; diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index 89458ac26288f..92f6118774ab2 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -124,6 +124,7 @@ import { AddCreatorIdToProjectTable1764276827837 } from '../common/1764276827837 import { CreateDynamicCredentialResolverTable1764682447000 } from '../common/1764682447000-CreateCredentialResolverTable'; import { AddDynamicCredentialEntryTable1764689388394 } from '../common/1764689388394-AddDynamicCredentialEntryTable'; import { BackfillMissingWorkflowHistoryRecords1765448186933 } from '../common/1765448186933-BackfillMissingWorkflowHistoryRecords'; +import { AddIconToAgentTable1765361177092 } from '../common/1765361177092-AddIconToAgentTable'; import type { Migration } from '../migration-types'; export const postgresMigrations: Migration[] = [ @@ -253,4 +254,5 @@ export const postgresMigrations: Migration[] = [ CreateDynamicCredentialResolverTable1764682447000, AddDynamicCredentialEntryTable1764689388394, BackfillMissingWorkflowHistoryRecords1765448186933, + AddIconToAgentTable1765361177092, ]; diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts index 778c5d3aab483..793e68a1a432b 100644 --- a/packages/@n8n/db/src/migrations/sqlite/index.ts +++ b/packages/@n8n/db/src/migrations/sqlite/index.ts @@ -120,6 +120,7 @@ import { CreateWorkflowPublishHistoryTable1764167920585 } from '../common/176416 import { CreateDynamicCredentialResolverTable1764682447000 } from '../common/1764682447000-CreateCredentialResolverTable'; import { AddDynamicCredentialEntryTable1764689388394 } from '../common/1764689388394-AddDynamicCredentialEntryTable'; import { BackfillMissingWorkflowHistoryRecords1765448186933 } from '../common/1765448186933-BackfillMissingWorkflowHistoryRecords'; +import { AddIconToAgentTable1765361177092 } from '../common/1765361177092-AddIconToAgentTable'; import type { Migration } from '../migration-types'; const sqliteMigrations: Migration[] = [ @@ -245,6 +246,7 @@ const sqliteMigrations: Migration[] = [ CreateDynamicCredentialResolverTable1764682447000, AddDynamicCredentialEntryTable1764689388394, BackfillMissingWorkflowHistoryRecords1765448186933, + AddIconToAgentTable1765361177092, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts index b858118d24f97..db1a1cd67b68b 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts @@ -1,4 +1,4 @@ -import { ChatHubLLMProvider } from '@n8n/api-types'; +import { ChatHubLLMProvider, AgentIconOrEmoji } from '@n8n/api-types'; import { WithTimestamps, User, CredentialsEntity, JsonColumn } from '@n8n/db'; import { Column, Entity, ManyToOne, JoinColumn, PrimaryGeneratedColumn } from '@n8n/typeorm'; import { INode } from 'n8n-workflow'; @@ -20,6 +20,12 @@ export class ChatHubAgent extends WithTimestamps { @Column({ type: 'varchar', length: 512, nullable: true }) description: string | null; + /** + * The icon or emoji for the chat agent. + */ + @JsonColumn() + icon: AgentIconOrEmoji; + /** * The system prompt for the chat agent. */ diff --git a/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts index 36a23bb4fcb22..9b6e4d705f1e6 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts @@ -1,8 +1,11 @@ -import { ChatModelsResponse } from '@n8n/api-types'; +import type { + ChatModelsResponse, + ChatHubUpdateAgentRequest, + ChatHubCreateAgentRequest, +} from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import type { User } from '@n8n/db'; import { Service } from '@n8n/di'; -import { INode } from 'n8n-workflow'; import { v4 as uuidv4 } from 'uuid'; import type { ChatHubAgent } from './chat-hub-agent.entity'; @@ -27,12 +30,14 @@ export class ChatHubAgentService { models: agents.map((agent) => ({ name: agent.name, description: agent.description ?? null, + icon: agent.icon, model: { provider: 'custom-agent', agentId: agent.id, }, createdAt: agent.createdAt.toISOString(), updatedAt: agent.updatedAt.toISOString(), + projectName: null, metadata: getModelMetadata(agent.provider, agent.model), })), }; @@ -50,18 +55,7 @@ export class ChatHubAgentService { return agent; } - async createAgent( - user: User, - data: { - name: string; - description?: string; - systemPrompt: string; - credentialId: string; - provider: ChatHubAgent['provider']; - model: string; - tools: INode[]; - }, - ): Promise { + async createAgent(user: User, data: ChatHubCreateAgentRequest): Promise { // Ensure user has access to credentials if provided await this.chatHubCredentialsService.ensureCredentialById(user, data.credentialId); @@ -71,6 +65,7 @@ export class ChatHubAgentService { id, name: data.name, description: data.description ?? null, + icon: data.icon, systemPrompt: data.systemPrompt, ownerId: user.id, credentialId: data.credentialId, @@ -86,15 +81,7 @@ export class ChatHubAgentService { async updateAgent( id: string, user: User, - updates: { - name?: string; - description?: string; - systemPrompt?: string; - credentialId?: string; - provider?: string; - model?: string; - tools?: INode[]; - }, + updates: ChatHubUpdateAgentRequest, ): Promise { // First check if the agent exists and belongs to the user const existingAgent = await this.chatAgentRepository.getOneById(id, user.id); @@ -110,6 +97,7 @@ export class ChatHubAgentService { const updateData: Partial = {}; if (updates.name !== undefined) updateData.name = updates.name; if (updates.description !== undefined) updateData.description = updates.description ?? null; + if (updates.icon !== undefined) updateData.icon = updates.icon; if (updates.systemPrompt !== undefined) updateData.systemPrompt = updates.systemPrompt; if (updates.credentialId !== undefined) updateData.credentialId = updates.credentialId ?? null; if (updates.provider !== undefined) diff --git a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts index 62dc013087f01..08ba4fa8b63bc 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts @@ -1,4 +1,4 @@ -import { ChatHubProvider } from '@n8n/api-types'; +import { ChatHubProvider, AgentIconOrEmoji } from '@n8n/api-types'; import { JsonColumn, WithTimestamps, @@ -104,6 +104,13 @@ export class ChatHubSession extends WithTimestamps { @Column({ type: 'varchar', length: 128, nullable: true }) agentName: string | null; + /** + * Cached icon of the agent/model. + * Used for all providers (LLM providers, custom agents, and n8n workflows). + */ + @JsonColumn({ nullable: true }) + agentIcon: AgentIconOrEmoji | null; + /** * All messages that belong to this chat session. */ diff --git a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts index baf699ed316e6..4af29dff5e0e0 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts @@ -1,4 +1,9 @@ -import { ChatHubConversationModel, ChatSessionId, type ChatHubInputModality } from '@n8n/api-types'; +import { + ChatHubConversationModel, + ChatSessionId, + type ChatHubInputModality, + type ChatModelDto, +} from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { SharedWorkflow, @@ -6,9 +11,10 @@ import { withTransaction, WorkflowEntity, WorkflowRepository, + type User, } from '@n8n/db'; import { Service } from '@n8n/di'; -import { EntityManager } from '@n8n/typeorm'; +import { EntityManager, In } from '@n8n/typeorm'; import { DateTime } from 'luxon'; import { AGENT_LANGCHAIN_NODE_TYPE, @@ -32,14 +38,21 @@ import { v4 as uuidv4 } from 'uuid'; import { ChatHubMessage } from './chat-hub-message.entity'; import { NODE_NAMES, PROVIDER_NODE_TYPE_MAP } from './chat-hub.constants'; -import { MessageRecord, type ContentBlock, type ChatTriggerResponseMode } from './chat-hub.types'; +import { + MessageRecord, + type ContentBlock, + type ChatTriggerResponseMode, + chatTriggerParamsShape, +} from './chat-hub.types'; import { getMaxContextWindowTokens } from './context-limits'; import { ChatHubAttachmentService } from './chat-hub.attachment.service'; +import { WorkflowService } from '@/workflows/workflow.service'; @Service() export class ChatHubWorkflowService { constructor( private readonly logger: Logger, + private readonly workflowService: WorkflowService, private readonly workflowRepository: WorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly chatHubAttachmentService: ChatHubAttachmentService, @@ -242,6 +255,109 @@ export class ChatHubWorkflowService { return Array.from(modalities); } + async fetchAgentWorkflowsAsModels(user: User): Promise { + // Workflows are scanned by their latest version for chat trigger nodes. + // This means that we might miss some active workflow versions that had chat triggers but + // the latest version does not, but this trade-off is done for performance. + const workflowsWithChatTrigger = await this.workflowService.getWorkflowsWithNodesIncluded( + user, + [CHAT_TRIGGER_NODE_TYPE], + true, + ); + + const activeWorkflows = workflowsWithChatTrigger + // Ensure the user has chat execution access to the workflow + .filter((workflow) => workflow.scopes.includes('workflow:execute-chat')) + // The workflow has to be active + .filter((workflow) => !!workflow.activeVersionId); + + return await this.fetchAgentWorkflowsAsModelsByIds( + activeWorkflows.map((workflow) => workflow.id), + ); + } + + async fetchAgentWorkflowsAsModelsByIds(ids: string[]): Promise { + const workflows = await this.workflowRepository.find({ + select: { + id: true, + name: true, + shared: { + role: true, + project: { + id: true, + name: true, + icon: { type: true, value: true }, + }, + }, + }, + where: { id: In(ids) }, + relations: { + activeVersion: true, + shared: { + project: true, + }, + }, + }); + + return workflows.flatMap((workflow) => { + const model = this.convertWorkflowToChatModel(workflow); + + return model ? [model] : []; + }); + } + + private convertWorkflowToChatModel({ + id, + name, + shared, + activeVersion, + }: WorkflowEntity): ChatModelDto | null { + if (!activeVersion) { + return null; + } + + const chatTrigger = activeVersion.nodes?.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE); + if (!chatTrigger) { + return null; + } + + const chatTriggerParams = chatTriggerParamsShape.safeParse(chatTrigger.parameters).data; + if (!chatTriggerParams?.availableInChat) { + return null; + } + + const inputModalities = this.parseInputModalities(chatTriggerParams.options); + + const agentName = + chatTriggerParams.agentName && chatTriggerParams.agentName.trim().length > 0 + ? chatTriggerParams.agentName + : name; + + // Find the owner's project (home project) + const ownerSharedWorkflow = shared?.find((sw) => sw.role === 'workflow:owner'); + const projectName = ownerSharedWorkflow?.project?.name ?? null; + + return { + name: agentName, + description: chatTriggerParams.agentDescription ?? null, + icon: ownerSharedWorkflow?.project?.icon ?? null, + model: { + provider: 'n8n', + workflowId: id, + }, + createdAt: activeVersion.createdAt ? activeVersion.createdAt.toISOString() : null, + updatedAt: activeVersion.updatedAt ? activeVersion.updatedAt.toISOString() : null, + projectName, + metadata: { + inputModalities, + capabilities: { + functionCalling: false, + }, + available: true, + }, + }; + } + private getUniqueNodeName(originalName: string, existingNames: Set): string { if (!existingNames.has(originalName)) { return originalName; diff --git a/packages/cli/src/modules/chat-hub/chat-hub.models.service.ts b/packages/cli/src/modules/chat-hub/chat-hub.models.service.ts index 10c2aae227bfa..037f83cca900a 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.models.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.models.service.ts @@ -7,31 +7,26 @@ import { type ChatModelDto, type ChatModelsResponse, } from '@n8n/api-types'; -import { In, WorkflowRepository, type User } from '@n8n/db'; +import { type User } from '@n8n/db'; import { Service } from '@n8n/di'; import { - CHAT_TRIGGER_NODE_TYPE, type INodeCredentials, type INodePropertyOptions, type IWorkflowExecuteAdditionalData, } from 'n8n-workflow'; import { ChatHubAgentService } from './chat-hub-agent.service'; -import { ChatHubWorkflowService } from './chat-hub-workflow.service'; import { getModelMetadata, PROVIDER_NODE_TYPE_MAP } from './chat-hub.constants'; -import { chatTriggerParamsShape } from './chat-hub.types'; import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; import { getBase } from '@/workflow-execute-additional-data'; -import { WorkflowService } from '@/workflows/workflow.service'; +import { ChatHubWorkflowService } from './chat-hub-workflow.service'; @Service() export class ChatHubModelsService { constructor( private readonly nodeParametersService: DynamicNodeParametersService, - private readonly workflowService: WorkflowService, - private readonly workflowRepository: WorkflowRepository, private readonly credentialsFinderService: CredentialsFinderService, private readonly chatHubAgentService: ChatHubAgentService, private readonly chatHubWorkflowService: ChatHubWorkflowService, @@ -158,7 +153,7 @@ export class ChatHubModelsService { return { models: this.transformAndFilterModels(rawModels, 'mistralCloud') }; } case 'n8n': - return await this.fetchAgentWorkflowsAsModels(user); + return { models: await this.chatHubWorkflowService.fetchAgentWorkflowsAsModels(user) }; case 'custom-agent': return await this.chatHubAgentService.getAgentsByUserIdAsModels(user.id); } @@ -712,78 +707,6 @@ export class ChatHubModelsService { ); } - private async fetchAgentWorkflowsAsModels(user: User): Promise { - // Workflows are scanned by their latest version for chat trigger nodes. - // This means that we might miss some active workflow versions that had chat triggers but - // the latest version does not, but this trade-off is done for performance. - const workflowsWithChatTrigger = await this.workflowService.getWorkflowsWithNodesIncluded( - user, - [CHAT_TRIGGER_NODE_TYPE], - true, - ); - - const activeWorkflows = workflowsWithChatTrigger - // Ensure the user has chat execution access to the workflow - .filter((workflow) => workflow.scopes.includes('workflow:execute-chat')) - // The workflow has to be active - .filter((workflow) => !!workflow.activeVersionId); - - const workflows = await this.workflowRepository.find({ - select: { id: true, name: true }, - where: { id: In(activeWorkflows.map((workflow) => workflow.id)) }, - relations: { activeVersion: true }, - }); - - const models: ChatModelDto[] = []; - - for (const { id, name, activeVersion } of workflows) { - if (!activeVersion) { - continue; - } - - const chatTrigger = activeVersion.nodes?.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE); - if (!chatTrigger) { - continue; - } - - const chatTriggerParams = chatTriggerParamsShape.safeParse(chatTrigger.parameters).data; - if (!chatTriggerParams?.availableInChat) { - continue; - } - - const inputModalities = this.chatHubWorkflowService.parseInputModalities( - chatTriggerParams.options, - ); - - const agentName = - chatTriggerParams.agentName && chatTriggerParams.agentName.trim().length > 0 - ? chatTriggerParams.agentName - : name; - - models.push({ - name: agentName, - description: chatTriggerParams.agentDescription ?? null, - model: { - provider: 'n8n', - workflowId: id, - }, - createdAt: activeVersion.createdAt ? activeVersion.createdAt.toISOString() : null, - updatedAt: activeVersion.updatedAt ? activeVersion.updatedAt.toISOString() : null, - metadata: { - inputModalities, - capabilities: { - functionCalling: false, - }, - available: true, - }, - }); - } - - return { - models, - }; - } - private transformAndFilterModels( rawModels: INodePropertyOptions[], provider: ChatHubLLMProvider, @@ -801,12 +724,14 @@ export class ChatHubModelsService { id, name: model.name, description: model.description ?? null, + icon: null, model: { provider, model: id, }, createdAt: null, updatedAt: null, + projectName: null, metadata, }, ]; diff --git a/packages/cli/src/modules/chat-hub/chat-hub.service.ts b/packages/cli/src/modules/chat-hub/chat-hub.service.ts index b8cc6ff46e347..edec2d32009e7 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -14,6 +14,7 @@ import { ChatHubN8nModel, ChatHubCustomAgentModel, type ChatHubUpdateConversationRequest, + type ChatModelDto, } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { GlobalConfig } from '@n8n/config'; @@ -170,15 +171,7 @@ export class ChatHubService { try { const result = await this.messageRepository.manager.transaction(async (trx) => { let session = await this.getChatSession(user, sessionId, trx); - session ??= await this.createChatSession( - user, - sessionId, - model, - credentialId, - tools, - payload.agentName, - trx, - ); + session ??= await this.createChatSession(user, sessionId, model, credentialId, tools, trx); await this.ensurePreviousMessage(previousMessageId, sessionId, trx); const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m])); @@ -1252,23 +1245,44 @@ export class ChatHubService { return await this.sessionRepository.getOneById(sessionId, user.id, trx); } + private async getAgentNameAndIcon( + user: User, + model: ChatHubConversationModel, + ): Promise> { + if (model.provider === 'custom-agent') { + return await this.chatHubAgentService.getAgentById(model.agentId, user.id); + } + + if (model.provider === 'n8n') { + const agents = await this.chatHubWorkflowService.fetchAgentWorkflowsAsModelsByIds([ + model.workflowId, + ]); + + return { icon: agents[0]?.icon ?? null, name: agents[0]?.name ?? '' }; + } + + return { icon: null, name: model.model }; + } + private async createChatSession( user: User, sessionId: ChatSessionId, model: ChatHubConversationModel, credentialId: string | null, tools: INode[], - agentName?: string, trx?: EntityManager, ) { await this.ensureValidModel(user, model); + const nameAndIcon = await this.getAgentNameAndIcon(user, model); + return await this.sessionRepository.createChatSession( { id: sessionId, ownerId: user.id, title: 'New Chat', - agentName, + agentName: nameAndIcon.name, + agentIcon: nameAndIcon.icon, tools, credentialId, ...model, @@ -1316,6 +1330,7 @@ export class ChatHubService { workflowId: session.workflowId, agentId: session.agentId, agentName: session.agentName ?? '', + agentIcon: session.agentIcon, createdAt: session.createdAt.toISOString(), updatedAt: session.updatedAt.toISOString(), tools: session.tools, @@ -1348,6 +1363,7 @@ export class ChatHubService { workflowId: session.workflowId, agentId: session.agentId, agentName: session.agentName ?? '', + agentIcon: session.agentIcon, createdAt: session.createdAt.toISOString(), updatedAt: session.updatedAt.toISOString(), tools: session.tools, @@ -1445,24 +1461,25 @@ export class ChatHubService { // Prepare the actual updates to be sent to the repository const sessionUpdates: Partial = {}; - if (updates.agent) { - const model = updates.agent.model; + if (updates.model) { + await this.ensureValidModel(user, updates.model); - await this.ensureValidModel(user, model); + const nameAndIcon = await this.getAgentNameAndIcon(user, updates.model); - sessionUpdates.agentName = updates.agent.name; - sessionUpdates.provider = model.provider; + sessionUpdates.agentName = nameAndIcon.name; + sessionUpdates.agentIcon = nameAndIcon.icon; + sessionUpdates.provider = updates.model.provider; sessionUpdates.model = null; sessionUpdates.credentialId = null; sessionUpdates.agentId = null; sessionUpdates.workflowId = null; - if (updates.agent.model.provider === 'n8n') { - sessionUpdates.workflowId = updates.agent.model.workflowId; - } else if (updates.agent.model.provider === 'custom-agent') { - sessionUpdates.agentId = updates.agent.model.agentId; + if (updates.model.provider === 'n8n') { + sessionUpdates.workflowId = updates.model.workflowId; + } else if (updates.model.provider === 'custom-agent') { + sessionUpdates.agentId = updates.model.agentId; } else { - sessionUpdates.model = updates.agent.model.model; + sessionUpdates.model = updates.model.model; } } diff --git a/packages/cli/src/modules/chat-hub/chat-hub.types.ts b/packages/cli/src/modules/chat-hub/chat-hub.types.ts index f40b0ff9ec966..6a81cdda9eb2e 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.types.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.types.ts @@ -31,7 +31,6 @@ export interface HumanMessagePayload extends BaseMessagePayload { previousMessageId: ChatMessageId | null; attachments: ChatAttachment[]; tools: INode[]; - agentName?: string; } export interface RegenerateMessagePayload extends BaseMessagePayload { retryId: ChatMessageId; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue b/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue index 701c68e048f21..340eb2b0d84e9 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue @@ -23,7 +23,7 @@ type BaseItem = { type Divider = { isDivider: true; id: string }; -type Item = BaseItem & { submenu?: Array }; +type Item = BaseItem & { submenu?: Array }; defineOptions({ name: 'N8nNavigationDropdown', @@ -133,6 +133,81 @@ defineExpose({ - {{ subitem.title }} + {{ subitem.title }} + + + @@ -240,6 +251,12 @@ defineExpose({ color: var(--color--text--tint-1); } + :global(.el-menu--horizontal .el-menu .el-menu-item) { + display: flex; + align-items: center; + gap: var(--spacing--2xs); + } + :global(.el-sub-menu__icon-arrow svg) { margin-top: auto; } @@ -261,4 +278,32 @@ defineExpose({ margin-right: var(--spacing--2xs); color: var(--color--text); } + +.menuItemWithTooltip { + position: relative; +} + +.menuItemTitle { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.infoTooltip { + flex-shrink: 0; + display: flex; + align-items: center; + padding-left: var(--spacing--xs); +} + +.infoIcon { + color: var(--color--text--tint-1); + cursor: pointer; + + &:hover { + color: var(--color--text); + } +} diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue index 8b2cb86c19d6f..56432a2d161ae 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue @@ -131,6 +131,9 @@ const menu = computed(() => { iconSize: 'large', title: truncateBeforeLast(agent.name, MAX_AGENT_NAME_CHARS_MENU), disabled: false, + description: agent.description + ? truncateBeforeLast(agent.description, 200, 0) + : undefined, }; }), ); @@ -147,6 +150,9 @@ const menu = computed(() => { iconSize: 'large', title: truncateBeforeLast(agent.name, MAX_AGENT_NAME_CHARS_MENU), disabled: false, + description: agent.description + ? truncateBeforeLast(agent.description, 200, 0) + : undefined, }; }); From ab79ad0b0a9de8b2864828ab13a9697ba6b20923 Mon Sep 17 00:00:00 2001 From: autologie Date: Mon, 15 Dec 2025 12:53:29 +0100 Subject: [PATCH 28/32] exclude FKs from migration --- .../1765788427674-AddIconToAgentTable.ts | 72 +------------------ .../chat-hub/chat-hub-session.entity.ts | 2 +- 2 files changed, 3 insertions(+), 71 deletions(-) diff --git a/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts b/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts index 6053fd6e42b3a..0a57759463412 100644 --- a/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts +++ b/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts @@ -7,80 +7,12 @@ const table = { } as const; export class AddIconToAgentTable1765788427674 implements ReversibleMigration { - async up({ - schemaBuilder: { addColumns, column, addForeignKey }, - runQuery, - isPostgres, - escape, - }: MigrationContext) { + async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { // Add icon column to agents table (nullable) await addColumns(table.agents, [column('icon').json]); - - // For PostgreSQL: convert agentId from varchar(36) to uuid to match agents.id type - if (isPostgres) { - await runQuery( - `ALTER TABLE ${escape.tableName(table.sessions)} ALTER COLUMN "agentId" TYPE uuid USING "agentId"::uuid`, - ); - await runQuery( - `ALTER TABLE ${escape.tableName(table.messages)} ALTER COLUMN "agentId" TYPE uuid USING "agentId"::uuid`, - ); - } - - // Clean up orphaned agentId references before adding foreign key constraint - await runQuery( - `UPDATE ${escape.tableName(table.sessions)} SET "agentId" = NULL WHERE "agentId" IS NOT NULL AND "agentId" NOT IN (SELECT id FROM ${escape.tableName(table.agents)})`, - ); - await runQuery( - `UPDATE ${escape.tableName(table.messages)} SET "agentId" = NULL WHERE "agentId" IS NOT NULL AND "agentId" NOT IN (SELECT id FROM ${escape.tableName(table.agents)})`, - ); - - // Add foreign key constraint for agentId in sessions table - await addForeignKey( - table.sessions, - 'agentId', - [table.agents, 'id'], - 'FK_chat_hub_sessions_agentId', - 'SET NULL', - ); - await addForeignKey( - table.messages, - 'agentId', - [table.agents, 'id'], - 'FK_chat_hub_messages_agentId', - 'SET NULL', - ); } - async down({ - schemaBuilder: { dropColumns, dropForeignKey }, - runQuery, - isPostgres, - escape, - }: MigrationContext) { - // Drop foreign key constraints - await dropForeignKey( - table.sessions, - 'agentId', - [table.agents, 'id'], - 'FK_chat_hub_sessions_agentId', - ); - await dropForeignKey( - table.messages, - 'agentId', - [table.agents, 'id'], - 'FK_chat_hub_messages_agentId', - ); - - // For PostgreSQL: revert agentId from uuid back to varchar(36) - if (isPostgres) { - await runQuery( - `ALTER TABLE ${escape.tableName(table.sessions)} ALTER COLUMN "agentId" TYPE varchar(36)`, - ); - await runQuery( - `ALTER TABLE ${escape.tableName(table.messages)} ALTER COLUMN "agentId" TYPE varchar(36)`, - ); - } - + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { // Drop icon column await dropColumns(table.agents, ['icon']); } diff --git a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts index fd61ec4b8c53d..e63a3c13cb23c 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts @@ -95,7 +95,7 @@ export class ChatHubSession extends WithTimestamps { * ID of the custom agent to use (if applicable). * Only set when provider is 'custom-agent'. */ - @Column({ type: 'uuid', nullable: true }) + @Column({ type: 'varchar', length: 36, nullable: true }) agentId: string | null; /** From bd4daeb8271f4f90c29be1c63d4bc7273ec498bb Mon Sep 17 00:00:00 2001 From: autologie Date: Mon, 15 Dec 2025 12:54:32 +0100 Subject: [PATCH 29/32] refactor --- .../common/1765788427674-AddIconToAgentTable.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts b/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts index 0a57759463412..5052ea85c1e93 100644 --- a/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts +++ b/packages/@n8n/db/src/migrations/common/1765788427674-AddIconToAgentTable.ts @@ -1,19 +1,15 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types'; -const table = { - agents: 'chat_hub_agents', - sessions: 'chat_hub_sessions', - messages: 'chat_hub_messages', -} as const; +const table = 'chat_hub_agents'; export class AddIconToAgentTable1765788427674 implements ReversibleMigration { async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { // Add icon column to agents table (nullable) - await addColumns(table.agents, [column('icon').json]); + await addColumns(table, [column('icon').json]); } async down({ schemaBuilder: { dropColumns } }: MigrationContext) { // Drop icon column - await dropColumns(table.agents, ['icon']); + await dropColumns(table, ['icon']); } } From c6e0246c57da44d7b50101f9bfb0a70e381aeef9 Mon Sep 17 00:00:00 2001 From: autologie Date: Mon, 15 Dec 2025 14:28:08 +0100 Subject: [PATCH 30/32] fix: add back migration for updating column type --- .../1765804780000-ConvertAgentIdToUuid.ts | 28 +++++++++++++++++++ .../db/src/migrations/postgresdb/index.ts | 2 ++ .../chat-hub/chat-hub-message.entity.ts | 2 +- .../chat-hub/chat-hub-session.entity.ts | 2 +- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 packages/@n8n/db/src/migrations/postgresdb/1765804780000-ConvertAgentIdToUuid.ts diff --git a/packages/@n8n/db/src/migrations/postgresdb/1765804780000-ConvertAgentIdToUuid.ts b/packages/@n8n/db/src/migrations/postgresdb/1765804780000-ConvertAgentIdToUuid.ts new file mode 100644 index 0000000000000..288d1777eb556 --- /dev/null +++ b/packages/@n8n/db/src/migrations/postgresdb/1765804780000-ConvertAgentIdToUuid.ts @@ -0,0 +1,28 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +const table = { + sessions: 'chat_hub_sessions', + messages: 'chat_hub_messages', +} as const; + +export class ConvertAgentIdToUuid1765804780000 implements ReversibleMigration { + async up({ runQuery, escape }: MigrationContext) { + // Convert agentId from varchar(36) to uuid to match agents.id type + await runQuery( + `ALTER TABLE ${escape.tableName(table.sessions)} ALTER COLUMN "agentId" TYPE uuid USING "agentId"::uuid`, + ); + await runQuery( + `ALTER TABLE ${escape.tableName(table.messages)} ALTER COLUMN "agentId" TYPE uuid USING "agentId"::uuid`, + ); + } + + async down({ runQuery, escape }: MigrationContext) { + // Revert agentId from uuid back to varchar(36) + await runQuery( + `ALTER TABLE ${escape.tableName(table.sessions)} ALTER COLUMN "agentId" TYPE varchar(36)`, + ); + await runQuery( + `ALTER TABLE ${escape.tableName(table.messages)} ALTER COLUMN "agentId" TYPE varchar(36)`, + ); + } +} diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index 06c58f35b2509..d3d15b61eb240 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -126,6 +126,7 @@ import { AddDynamicCredentialEntryTable1764689388394 } from '../common/176468938 import { BackfillMissingWorkflowHistoryRecords1765448186933 } from '../common/1765448186933-BackfillMissingWorkflowHistoryRecords'; import { AddResolvableFieldsToCredentials1765459448000 } from '../common/1765459448000-AddResolvableFieldsToCredentials'; import { AddIconToAgentTable1765788427674 } from '../common/1765788427674-AddIconToAgentTable'; +import { ConvertAgentIdToUuid1765804780000 } from './1765804780000-ConvertAgentIdToUuid'; import type { Migration } from '../migration-types'; export const postgresMigrations: Migration[] = [ @@ -257,4 +258,5 @@ export const postgresMigrations: Migration[] = [ BackfillMissingWorkflowHistoryRecords1765448186933, AddResolvableFieldsToCredentials1765459448000, AddIconToAgentTable1765788427674, + ConvertAgentIdToUuid1765804780000, ]; diff --git a/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts index b433c937f7244..1a58ac637a25c 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts @@ -81,7 +81,7 @@ export class ChatHubMessage extends WithTimestamps { * ID of the custom agent that produced this message (if applicable). * Only set when provider is 'custom-agent'. */ - @Column({ type: 'varchar', length: 36, nullable: true }) + @Column({ type: 'uuid', nullable: true }) agentId: string | null; /** diff --git a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts index e63a3c13cb23c..fd61ec4b8c53d 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts @@ -95,7 +95,7 @@ export class ChatHubSession extends WithTimestamps { * ID of the custom agent to use (if applicable). * Only set when provider is 'custom-agent'. */ - @Column({ type: 'varchar', length: 36, nullable: true }) + @Column({ type: 'uuid', nullable: true }) agentId: string | null; /** From 328d3c1ab4032fdc84398464f189079723673ec6 Mon Sep 17 00:00:00 2001 From: autologie Date: Mon, 15 Dec 2025 14:37:50 +0100 Subject: [PATCH 31/32] lint --- packages/@n8n/db/src/migrations/postgresdb/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index d3d15b61eb240..ebca1b497f788 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -46,6 +46,7 @@ import { AddProjectIdToVariableTable1758794506893 } from './1758794506893-AddPro import { AddWorkflowVersionColumn1761047826451 } from './1761047826451-AddWorkflowVersionColumn'; import { ChangeDependencyInfoToJson1761655473000 } from './1761655473000-ChangeDependencyInfoToJson'; import { ChangeDefaultForIdInUserTable1762771264000 } from './1762771264000-ChangeDefaultForIdInUserTable'; +import { ConvertAgentIdToUuid1765804780000 } from './1765804780000-ConvertAgentIdToUuid'; import { CreateLdapEntities1674509946020 } from '../common/1674509946020-CreateLdapEntities'; import { PurgeInvalidWorkflowConnections1675940580449 } from '../common/1675940580449-PurgeInvalidWorkflowConnections'; import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns'; @@ -126,7 +127,6 @@ import { AddDynamicCredentialEntryTable1764689388394 } from '../common/176468938 import { BackfillMissingWorkflowHistoryRecords1765448186933 } from '../common/1765448186933-BackfillMissingWorkflowHistoryRecords'; import { AddResolvableFieldsToCredentials1765459448000 } from '../common/1765459448000-AddResolvableFieldsToCredentials'; import { AddIconToAgentTable1765788427674 } from '../common/1765788427674-AddIconToAgentTable'; -import { ConvertAgentIdToUuid1765804780000 } from './1765804780000-ConvertAgentIdToUuid'; import type { Migration } from '../migration-types'; export const postgresMigrations: Migration[] = [ From 2f6ce74dc0a6b6ea376deda1a82be6a2715934ef Mon Sep 17 00:00:00 2001 From: autologie Date: Mon, 15 Dec 2025 14:49:19 +0100 Subject: [PATCH 32/32] fix type --- .../frontend/editor-ui/src/features/ai/chatHub/constants.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts index b471787c68f13..0770da1935acf 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts @@ -1,4 +1,4 @@ -import type { ChatHubLLMProvider } from '@n8n/api-types'; +import type { ChatHubProvider } from '@n8n/api-types'; // Route and view identifiers export const CHAT_VIEW = 'chat'; @@ -9,7 +9,7 @@ export const CHAT_SETTINGS_VIEW = 'chat-settings'; export const CHAT_STORE = 'chatStore'; -export const providerDisplayNames: Record = { +export const providerDisplayNames: Record = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google', @@ -24,6 +24,8 @@ export const providerDisplayNames: Record = { deepSeek: 'DeepSeek', cohere: 'Cohere', mistralCloud: 'Mistral Cloud', + n8n: 'n8n', + 'custom-agent': 'Custom Agent', }; export const MOBILE_MEDIA_QUERY = '(max-width: 768px)';