Show tool execution messages in AI agent chat (#13117)

https://github.com/user-attachments/assets/c0a42726-50ac-496e-a993-9d6076a84a6a

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-07-10 11:15:05 +05:30
committed by GitHub
parent e6cdae5c27
commit 8310b4ff01
62 changed files with 1304 additions and 227 deletions

View File

@ -1042,6 +1042,7 @@ export enum MessageChannelVisibility {
export enum ModelProvider {
ANTHROPIC = 'ANTHROPIC',
NONE = 'NONE',
OPENAI = 'OPENAI'
}
@ -2785,6 +2786,7 @@ export type Workspace = {
customDomain?: Maybe<Scalars['String']>;
databaseSchema: Scalars['String'];
databaseUrl: Scalars['String'];
defaultAgent?: Maybe<Agent>;
defaultRole?: Maybe<Role>;
deletedAt?: Maybe<Scalars['DateTime']>;
displayName?: Maybe<Scalars['String']>;
@ -3599,7 +3601,7 @@ export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{
export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', getServerlessFunctionSourceCode?: any | null };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null };
@ -3618,7 +3620,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@ -4042,6 +4044,9 @@ export const UserQueryFragmentFragmentDoc = gql`
defaultRole {
...RoleFragment
}
defaultAgent {
id
}
}
availableWorkspaces {
...AvailableWorkspacesFragment

View File

@ -999,6 +999,7 @@ export enum MessageChannelVisibility {
export enum ModelProvider {
ANTHROPIC = 'ANTHROPIC',
NONE = 'NONE',
OPENAI = 'OPENAI'
}
@ -2613,6 +2614,7 @@ export type Workspace = {
customDomain?: Maybe<Scalars['String']>;
databaseSchema: Scalars['String'];
databaseUrl: Scalars['String'];
defaultAgent?: Maybe<Agent>;
defaultRole?: Maybe<Role>;
deletedAt?: Maybe<Scalars['DateTime']>;
displayName?: Maybe<Scalars['String']>;

View File

@ -6,7 +6,7 @@ import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { msg } from '@lingui/core/macro';
import { IconSearch } from 'twenty-ui/display';
import { IconSearch, IconSparkles } from 'twenty-ui/display';
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
[RecordAgnosticActionsKeys.SEARCH_RECORDS]: {
@ -50,4 +50,24 @@ export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
hotKeys: ['/'],
shouldBeRegistered: () => true,
},
[RecordAgnosticActionsKeys.ASK_AI]: {
type: ActionType.Standard,
scope: ActionScope.Global,
key: RecordAgnosticActionsKeys.ASK_AI,
label: msg`Ask AI`,
shortLabel: msg`Ask AI`,
position: 2,
isPinned: false,
Icon: IconSparkles,
availableOn: [ActionViewType.GLOBAL],
component: (
<ActionOpenSidePanelPage
page={CommandMenuPages.AskAI}
pageTitle="Ask AI"
pageIcon={IconSparkles}
/>
),
hotKeys: ['@'],
shouldBeRegistered: () => true,
},
};

View File

@ -0,0 +1,25 @@
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
import { RecordAgnosticActionsKeys } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys';
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const useRecordAgnosticActions = () => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const actions: Record<string, ActionConfig> = {
[RecordAgnosticActionsKeys.SEARCH_RECORDS]:
RECORD_AGNOSTIC_ACTIONS_CONFIG[RecordAgnosticActionsKeys.SEARCH_RECORDS],
[RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK]:
RECORD_AGNOSTIC_ACTIONS_CONFIG[
RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK
],
};
if (isAiEnabled) {
actions[RecordAgnosticActionsKeys.ASK_AI] =
RECORD_AGNOSTIC_ACTIONS_CONFIG[RecordAgnosticActionsKeys.ASK_AI];
}
return actions;
};

View File

@ -1,4 +1,5 @@
export enum RecordAgnosticActionsKeys {
SEARCH_RECORDS = 'search-records',
SEARCH_RECORDS_FALLBACK = 'search-records-fallback',
ASK_AI = 'ask-ai',
}

View File

@ -1,4 +1,4 @@
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
@ -30,7 +30,7 @@ export const useRegisteredActions = (
objectMetadataItem,
});
const recordAgnosticActionConfig = RECORD_AGNOSTIC_ACTIONS_CONFIG;
const recordAgnosticActionConfig = useRecordAgnosticActions();
const actionsConfig = {
...recordActionConfig,

View File

@ -2,7 +2,10 @@ import {
ApolloClient,
ApolloClientOptions,
ApolloLink,
FetchResult,
fromPromise,
Observable,
Operation,
ServerError,
ServerParseError,
} from '@apollo/client';
@ -19,7 +22,12 @@ import { AuthTokenPair } from '~/generated/graphql';
import { logDebug } from '~/utils/logDebug';
import { i18n } from '@lingui/core';
import { GraphQLFormattedError } from 'graphql';
import {
DefinitionNode,
DirectiveNode,
GraphQLFormattedError,
SelectionNode,
} from 'graphql';
import isEmpty from 'lodash.isempty';
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
@ -115,45 +123,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
},
attempts: {
max: 2,
retryIf: (error) => !!error,
retryIf: (error) => {
if (this.isAuthenticationError(error)) {
return false;
}
return Boolean(error);
},
},
});
const handleTokenRenewal = (
operation: Operation,
forward: (operation: Operation) => Observable<FetchResult>,
) => {
return fromPromise(
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
}
})
.catch(() => {
onUnauthenticatedError?.();
}),
).flatMap(() => forward(operation));
};
const errorLink = onError(
({ graphQLErrors, networkError, forward, operation }) => {
if (isDefined(graphQLErrors)) {
onErrorCb?.(graphQLErrors);
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
return fromPromise(
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
}
})
.catch(() => {
onUnauthenticatedError?.();
}),
).flatMap(() => forward(operation));
return handleTokenRenewal(operation, forward);
}
switch (graphQLError?.extensions?.code) {
case 'UNAUTHENTICATED': {
return fromPromise(
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem(
'tokenPair',
JSON.stringify(tokens),
);
}
})
.catch(() => {
onUnauthenticatedError?.();
}),
).flatMap(() => forward(operation));
return handleTokenRenewal(operation, forward);
}
case 'FORBIDDEN': {
return;
@ -220,6 +228,13 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
}
if (isDefined(networkError)) {
if (
this.isRestOperation(operation) &&
this.isAuthenticationError(networkError as ServerError)
) {
return handleTokenRenewal(operation, forward);
}
if (isDebugMode === true) {
logDebug(`[Network error]: ${networkError}`);
}
@ -248,6 +263,26 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
}
private isRestOperation(operation: Operation): boolean {
return operation.query.definitions.some(
(def: DefinitionNode) =>
def.kind === 'OperationDefinition' &&
def.selectionSet?.selections.some(
(selection: SelectionNode) =>
selection.kind === 'Field' &&
selection.directives?.some(
(directive: DirectiveNode) =>
directive.name.value === 'rest' ||
directive.name.value === 'stream',
),
),
);
}
private isAuthenticationError(error: ServerError): boolean {
return error.statusCode === 401;
}
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
this.currentWorkspaceMember = workspaceMember;
}

View File

@ -1,4 +1,9 @@
import { ApolloLink, Observable, Operation } from '@apollo/client/core';
import {
ApolloLink,
Observable,
Operation,
ServerError,
} from '@apollo/client/core';
import { FetchResult } from '@apollo/client/link/core';
import { ArgumentNode, DirectiveNode } from 'graphql';
import { isDefined } from 'twenty-shared/utils';
@ -57,7 +62,13 @@ export class StreamingRestLink extends ApolloLink {
fetch(url, requestConfig)
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
const networkError = new Error(
`HTTP error! status: ${response.status}`,
) as ServerError;
networkError.statusCode = response.status;
throw networkError;
}
if (!response.body) {
@ -66,7 +77,6 @@ export class StreamingRestLink extends ApolloLink {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedData = '';
let isStreaming = true;
while (isStreaming) {
@ -79,19 +89,9 @@ export class StreamingRestLink extends ApolloLink {
}
const decodedChunk = decoder.decode(value, { stream: true });
accumulatedData += decodedChunk;
if (isDefined(onChunk) && typeof onChunk === 'function') {
onChunk(accumulatedData);
}
try {
const parsedData = JSON.parse(decodedChunk);
observer.next({ data: parsedData });
} catch {
observer.next({
data: { streamingData: decodedChunk },
});
onChunk(decodedChunk);
}
}
})

View File

@ -25,6 +25,7 @@ export type CurrentWorkspace = Pick<
| 'metadataVersion'
> & {
defaultRole?: Omit<Role, 'workspaceMembers'> | null;
defaultAgent?: { id: string } | null;
};
export const currentWorkspaceState = createState<CurrentWorkspace | null>({

View File

@ -1,4 +1,5 @@
import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/CommandMenuAskAIPage';
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
@ -32,4 +33,5 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
[CommandMenuPages.WorkflowStepView, <CommandMenuWorkflowViewStep />],
[CommandMenuPages.WorkflowRunStepView, <CommandMenuWorkflowRunViewStep />],
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
[CommandMenuPages.AskAI, <CommandMenuAskAIPage />],
]);

View File

@ -2,6 +2,7 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
@ -12,15 +13,19 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { FeatureFlagKey } from '~/generated/graphql';
export const useCommandMenuHotKeys = () => {
const { toggleCommandMenu } = useCommandMenu();
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
const { goBackFromCommandMenu } = useCommandMenuHistory();
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
@ -31,6 +36,8 @@ export const useCommandMenuHotKeys = () => {
const commandMenuPage = useRecoilValue(commandMenuPageState);
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const contextStoreTargetedRecordsRuleComponent = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
COMMAND_MENU_COMPONENT_INSTANCE_ID,
@ -58,6 +65,20 @@ export const useCommandMenuHotKeys = () => {
},
);
useGlobalHotkeys(
['@'],
() => {
if (isAiEnabled) {
openAskAIPage();
}
},
false,
[openAskAIPage, isAiEnabled],
{
ignoreModifiers: true,
},
);
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {

View File

@ -0,0 +1,22 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { IconSparkles } from 'twenty-ui/display';
import { v4 } from 'uuid';
export const useOpenAskAIPageInCommandMenu = () => {
const { navigateCommandMenu } = useCommandMenu();
const openAskAIPage = () => {
navigateCommandMenu({
page: CommandMenuPages.AskAI,
pageTitle: t`Ask AI`,
pageIcon: IconSparkles,
pageId: v4(),
});
};
return {
openAskAIPage,
};
};

View File

@ -0,0 +1,37 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AIChatTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
const StyledContainer = styled.div`
height: 100%;
width: 100%;
`;
const StyledEmptyState = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
height: 100%;
justify-content: center;
`;
export const CommandMenuAskAIPage = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const agentId = currentWorkspace?.defaultAgent?.id;
if (!agentId) {
return (
<StyledContainer>
<StyledEmptyState>No AI Agent found.</StyledEmptyState>
</StyledContainer>
);
}
return (
<StyledContainer>
<AIChatTab agentId={agentId} />
</StyledContainer>
);
};

View File

@ -11,4 +11,5 @@ export enum CommandMenuPages {
WorkflowStepEdit = 'workflow-step-edit',
WorkflowRunStepView = 'workflow-run-step-view',
SearchRecords = 'search-records',
AskAI = 'ask-ai',
}

View File

@ -1,14 +1,17 @@
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { SettingsPath } from '@/types/SettingsPath';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { IconSearch, IconSettings } from 'twenty-ui/display';
import { IconSearch, IconSettings, IconSparkles } from 'twenty-ui/display';
import { useIsMobile } from 'twenty-ui/utilities';
import { FeatureFlagKey } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const MainNavigationDrawerFixedItems = () => {
@ -29,6 +32,9 @@ export const MainNavigationDrawerFixedItems = () => {
const { t } = useLingui();
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
return (
!isMobile && (
<>
@ -39,6 +45,15 @@ export const MainNavigationDrawerFixedItems = () => {
keyboard={['/']}
mouseUpNavigation={true}
/>
{isAiEnabled && (
<NavigationDrawerItem
label={t`Ask AI`}
Icon={IconSparkles}
onClick={openAskAIPage}
keyboard={['@']}
mouseUpNavigation={true}
/>
)}
<NavigationDrawerItem
label={t`Settings`}
to={getSettingsPath(SettingsPath.ProfilePage)}

View File

@ -93,6 +93,8 @@ export const UserProviderEffect = () => {
setCurrentWorkspace({
...queryData.currentUser.currentWorkspace,
defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null,
defaultAgent:
queryData.currentUser.currentWorkspace.defaultAgent ?? null,
});
}

View File

@ -1,13 +1,13 @@
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
import { gql } from '@apollo/client';
import {
AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT,
AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT,
} from '@/auth/graphql/fragments/authFragments';
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
import { gql } from '@apollo/client';
export const USER_QUERY_FRAGMENT = gql`
${ROLE_FRAGMENT}
@ -92,6 +92,9 @@ export const USER_QUERY_FRAGMENT = gql`
defaultRole {
...RoleFragment
}
defaultAgent {
id
}
}
availableWorkspaces {
...AvailableWorkspacesFragment

View File

@ -1,5 +1,5 @@
import { TextArea } from '@/ui/input/components/TextArea';
import { useTheme } from '@emotion/react';
import { keyframes, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React from 'react';
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
@ -11,6 +11,7 @@ import { t } from '@lingui/core/macro';
import { Button } from 'twenty-ui/input';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { useAgentChat } from '../hooks/useAgentChat';
import { AgentChatMessage } from '../hooks/useAgentChatMessages';
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
const StyledContainer = styled.div`
@ -86,9 +87,11 @@ const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
}
`;
const StyledMessageRow = styled.div`
const StyledMessageRow = styled.div<{ isShowingToolCall?: boolean }>`
display: flex;
flex-direction: row;
align-items: ${({ isShowingToolCall }) =>
isShowingToolCall ? 'center' : 'flex-start'};
gap: ${({ theme }) => theme.spacing(3)};
width: 100%;
`;
@ -152,6 +155,23 @@ const StyledMessageContainer = styled.div`
width: 100%;
`;
const dots = keyframes`
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
`;
const StyledToolCallContainer = styled.div`
&::after {
display: inline-block;
content: '';
animation: ${dots} 750ms steps(3, end) infinite;
width: 2ch;
text-align: left;
}
`;
type AIChatTabProps = {
agentId: string;
};
@ -168,16 +188,49 @@ export const AIChatTab: React.FC<AIChatTabProps> = ({ agentId }) => {
agentStreamingMessage,
} = useAgentChat(agentId);
const getAssistantMessageContent = (message: AgentChatMessage) => {
if (message.content !== '') {
return message.content;
}
if (agentStreamingMessage.streamingText !== '') {
return agentStreamingMessage.streamingText;
}
if (agentStreamingMessage.toolCall !== '') {
return (
<StyledToolCallContainer>
{agentStreamingMessage.toolCall}
</StyledToolCallContainer>
);
}
return (
<StyledDotsIconContainer>
<StyledDotsIcon size={theme.icon.size.xl} />
</StyledDotsIconContainer>
);
};
return (
<StyledContainer>
{messages.length !== 0 && (
<StyledScrollWrapper componentInstanceId={agentId}>
<StyledScrollWrapper
componentInstanceId={`scroll-wrapper-ai-chat-${agentId}`}
>
{messages.map((msg) => (
<StyledMessageBubble
key={msg.id}
isUser={msg.role === AgentChatMessageRole.USER}
>
<StyledMessageRow>
<StyledMessageRow
isShowingToolCall={
msg.role === AgentChatMessageRole.ASSISTANT &&
msg.content === '' &&
agentStreamingMessage.streamingText === '' &&
agentStreamingMessage.toolCall !== ''
}
>
{msg.role === AgentChatMessageRole.ASSISTANT && (
<StyledAvatarContainer>
<Avatar
@ -197,12 +250,8 @@ export const AIChatTab: React.FC<AIChatTabProps> = ({ agentId }) => {
<StyledMessageText
isUser={msg.role === AgentChatMessageRole.USER}
>
{msg.role === AgentChatMessageRole.ASSISTANT && !msg.content
? agentStreamingMessage || (
<StyledDotsIconContainer>
<StyledDotsIcon size={theme.icon.size.xl} />
</StyledDotsIconContainer>
)
{msg.role === AgentChatMessageRole.ASSISTANT
? getAssistantMessageContent(msg)
: msg.content}
</StyledMessageText>
{msg.content && (

View File

@ -8,11 +8,11 @@ import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWr
import { STREAM_CHAT_QUERY } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api';
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
import { useApolloClient } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { agentChatInputState } from '../states/agentChatInputState';
import { agentChatMessagesComponentState } from '../states/agentChatMessagesComponentState';
import { agentStreamingMessageState } from '../states/agentStreamingMessageState';
import { parseAgentStreamingChunk } from '../utils/parseAgentStreamingChunk';
import { AgentChatMessage, useAgentChatMessages } from './useAgentChatMessages';
import { useAgentChatThreads } from './useAgentChatThreads';
@ -50,21 +50,14 @@ export const useAgentChat = (agentId: string) => {
useAgentChatThreads(agentId);
const currentThreadId = threads[0]?.id;
const {
data: messagesData,
loading: messagesLoading,
refetch: refetchMessages,
} = useAgentChatMessages(currentThreadId);
const { loading: messagesLoading, refetch: refetchMessages } =
useAgentChatMessages(currentThreadId, ({ messages }) => {
setAgentChatMessages(messages);
scrollToBottom();
});
const isLoading = messagesLoading || threadsLoading || isStreaming;
if (
agentChatMessages.length === 0 &&
isDefined(messagesData?.messages?.length)
) {
setAgentChatMessages(messagesData.messages);
}
const createOptimisticMessages = (content: string): AgentChatMessage[] => {
const optimisticUserMessage: OptimisticMessage = {
id: v4(),
@ -104,8 +97,22 @@ export const useAgentChat = (agentId: string) => {
},
context: {
onChunk: (chunk: string) => {
setAgentStreamingMessage(chunk);
scrollToBottom();
parseAgentStreamingChunk(chunk, {
onTextDelta: (message: string) => {
setAgentStreamingMessage((prev) => ({
...prev,
streamingText: prev.streamingText + message,
}));
scrollToBottom();
},
onToolCall: (message: string) => {
setAgentStreamingMessage((prev) => ({
...prev,
toolCall: message,
}));
scrollToBottom();
},
});
},
},
});
@ -128,7 +135,10 @@ export const useAgentChat = (agentId: string) => {
const { data } = await refetchMessages();
setAgentChatMessages(data?.messages);
setAgentStreamingMessage('');
setAgentStreamingMessage({
toolCall: '',
streamingText: '',
});
scrollToBottom();
};

View File

@ -11,9 +11,13 @@ export type AgentChatMessage = {
createdAt: string;
};
export const useAgentChatMessages = (threadId: string) => {
export const useAgentChatMessages = (
threadId: string,
onCompleted?: (data: { messages: AgentChatMessage[] }) => void,
) => {
return useQuery<{ messages: AgentChatMessage[] }>(GET_AGENT_CHAT_MESSAGES, {
variables: { threadId },
skip: !isDefined(threadId),
onCompleted,
});
};

View File

@ -1,6 +1,12 @@
import { atom } from 'recoil';
export const agentStreamingMessageState = atom<string>({
export const agentStreamingMessageState = atom<{
toolCall: string;
streamingText: string;
}>({
key: 'agentStreamingMessageState',
default: '',
default: {
toolCall: '',
streamingText: '',
},
});

View File

@ -0,0 +1,43 @@
export type AgentStreamingEvent = {
type: 'text-delta' | 'tool-call';
message: string;
};
export type AgentStreamingParserCallbacks = {
onTextDelta?: (message: string) => void;
onToolCall?: (message: string) => void;
onError?: (error: Error, rawLine: string) => void;
};
export const parseAgentStreamingChunk = (
chunk: string,
callbacks: AgentStreamingParserCallbacks,
): void => {
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim() !== '') {
try {
const event = JSON.parse(line) as AgentStreamingEvent;
switch (event.type) {
case 'text-delta':
callbacks.onTextDelta?.(event.message);
break;
case 'tool-call':
callbacks.onToolCall?.(event.message);
break;
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to parse stream event:', error);
const errorMessage =
error instanceof Error
? error
: new Error(`Unknown parsing error: ${String(error)}`);
callbacks.onError?.(errorMessage, line);
}
}
}
};