fix(ai-chat) - improvements (#22193)

- remove flickering at assistant message streamed end
- add copy code
- leave chat history when navigating to settings

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22193?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-06-26 10:26:40 +02:00
committed by GitHub
parent e60d790990
commit b625bd1995
8 changed files with 155 additions and 14 deletions
@@ -1,18 +1,17 @@
import { Logger, Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { createUIMessageStream } from 'ai';
import type {
CodeExecutionData,
ExtendedUIMessage,
ExtendedUIMessagePart,
} from 'twenty-shared/ai';
import { isNonEmptyString } from '@sniptt/guards';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
@@ -30,6 +29,8 @@ import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/service
import { ChatExecutionService } from 'src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service';
import { getCancelChannel } from 'src/engine/metadata-modules/ai/ai-chat/utils/get-cancel-channel.util';
import type { AiModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-config.type';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { STREAM_AGENT_CHAT_JOB_NAME } from './stream-agent-chat-job-name.constant';
import { type StreamAgentChatJobData } from './stream-agent-chat-job.types';
@@ -184,6 +185,8 @@ export class StreamAgentChatJob {
abortSignal: AbortSignal;
}): Promise<void> {
return new Promise<void>((resolve, reject) => {
const assistantMessageId = v4();
let streamUsage = {
inputTokens: 0,
outputTokens: 0,
@@ -259,7 +262,8 @@ export class StreamAgentChatJob {
return error instanceof Error ? error.message : String(error);
},
sendStart: false,
sendStart: true,
generateMessageId: () => assistantMessageId,
messageMetadata: ({ part }) => {
return this.computeMessageMetadata({
part,
@@ -280,6 +284,7 @@ export class StreamAgentChatJob {
onFinish: async ({ responseMessage, isAborted }) => {
try {
await this.handleStreamFinish({
assistantMessageId,
responseMessage,
isAborted,
streamError,
@@ -335,7 +340,10 @@ export class StreamAgentChatJob {
await this.eventPublisherService.publish({
threadId: data.threadId,
workspaceId: data.workspaceId,
event: { type: 'message-persisted', messageId: data.threadId },
event: {
type: 'message-persisted',
messageId: assistantMessageId,
},
});
resolve();
}
@@ -435,6 +443,7 @@ export class StreamAgentChatJob {
}
private async handleStreamFinish({
assistantMessageId,
responseMessage,
isAborted,
streamError,
@@ -448,6 +457,7 @@ export class StreamAgentChatJob {
modelConfig,
userMessagePromise,
}: {
assistantMessageId: string;
responseMessage: Omit<ExtendedUIMessage, 'id'>;
isAborted: boolean;
streamError: unknown;
@@ -502,6 +512,7 @@ export class StreamAgentChatJob {
await this.agentChatService.addMessage({
threadId,
uiMessage: responseMessage,
id: assistantMessageId,
turnId: userMessage.turnId ?? undefined,
workspaceId,
});