From 024192d1b5e79fdd99bff04a06ceb3e353836d9b Mon Sep 17 00:00:00 2001
From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:37:31 +0300
Subject: [PATCH 1/8] fix(root): resolve high/moderate liquidjs and nodemailer
vulnerabilities (#10625)
Co-authored-by: Cursor Agent
Co-authored-by: Dima Grossman
---
apps/api/package.json | 2 +-
apps/dashboard/package.json | 2 +-
enterprise/packages/translation/package.json | 2 +-
libs/application-generic/package.json | 2 +-
packages/framework/package.json | 2 +-
packages/providers/package.json | 2 +-
pnpm-lock.yaml | 42 ++++++++++----------
7 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/apps/api/package.json b/apps/api/package.json
index f72d34b60ac..48c7c013935 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -101,7 +101,7 @@
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.0.0",
"jsonwebtoken": "9.0.3",
- "liquidjs": "^10.25.0",
+ "liquidjs": "^10.25.5",
"lodash": "^4.18.0",
"lru-cache": "^11.2.4",
"nanoid": "^3.1.20",
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 1854b3d08c7..5f561ce2f5f 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -110,7 +110,7 @@
"json-schema": "^0.4.0",
"json5": "^2.2.3",
"launchdarkly-react-client-sdk": "^3.9.0",
- "liquidjs": "^10.25.0",
+ "liquidjs": "^10.25.5",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
diff --git a/enterprise/packages/translation/package.json b/enterprise/packages/translation/package.json
index 64847b619bc..a0a1df452e4 100644
--- a/enterprise/packages/translation/package.json
+++ b/enterprise/packages/translation/package.json
@@ -40,7 +40,7 @@
"sinon": "^9.2.4",
"ts-node": "~10.9.1",
"typescript": "5.6.2",
- "liquidjs": "^10.25.0"
+ "liquidjs": "^10.25.5"
},
"peerDependencies": {
"@nestjs/common": "10.4.18",
diff --git a/libs/application-generic/package.json b/libs/application-generic/package.json
index 1e2cb9f6266..99e5cc014e8 100644
--- a/libs/application-generic/package.json
+++ b/libs/application-generic/package.json
@@ -95,7 +95,7 @@
"json-schema-to-ts": "^3.0.0",
"json-schema-faker": "^0.5.6",
"jsonwebtoken": "9.0.3",
- "liquidjs": "^10.25.0",
+ "liquidjs": "^10.25.5",
"lodash": "^4.18.0",
"lru-cache": "^11.2.4",
"mixpanel": "^0.17.0",
diff --git a/packages/framework/package.json b/packages/framework/package.json
index 60a9e4f8ebb..340e31ebf5e 100644
--- a/packages/framework/package.json
+++ b/packages/framework/package.json
@@ -259,7 +259,7 @@
"cross-fetch": "^4.0.0",
"json-schema-to-ts": "^3.0.0",
"jsonrepair": "^3.13.1",
- "liquidjs": "^10.25.0",
+ "liquidjs": "^10.25.5",
"pluralize": "^8.0.0",
"sanitize-html": "^2.13.0"
},
diff --git a/packages/providers/package.json b/packages/providers/package.json
index e907c686a6e..49d0d14d32f 100644
--- a/packages/providers/package.json
+++ b/packages/providers/package.json
@@ -66,7 +66,7 @@
"nanoid": "^3.1.20",
"node-fetch": "^3.2.10",
"node-mailjet": "^6.0.8",
- "nodemailer": "^8.0.4",
+ "nodemailer": "^8.0.5",
"plivo": "^4.70.0",
"postmark": "^4.0.2",
"proxy-agent": "^6.5.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1b78d1ff903..95ca54cdabb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -482,8 +482,8 @@ importers:
specifier: 9.0.3
version: 9.0.3
liquidjs:
- specifier: ^10.25.0
- version: 10.25.0
+ specifier: ^10.25.5
+ version: 10.25.5
lodash:
specifier: ^4.18.0
version: 4.18.1
@@ -925,8 +925,8 @@ importers:
specifier: ^3.9.0
version: 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
liquidjs:
- specifier: ^10.25.0
- version: 10.25.0
+ specifier: ^10.25.5
+ version: 10.25.5
lodash.debounce:
specifier: ^4.0.8
version: 4.0.8
@@ -2326,8 +2326,8 @@ importers:
specifier: ^7.0.3
version: 7.0.3
liquidjs:
- specifier: ^10.25.0
- version: 10.25.0
+ specifier: ^10.25.5
+ version: 10.25.5
mocha:
specifier: ^8.1.1
version: 8.4.0
@@ -2545,8 +2545,8 @@ importers:
specifier: 9.0.3
version: 9.0.3
liquidjs:
- specifier: ^10.25.0
- version: 10.25.0
+ specifier: ^10.25.5
+ version: 10.25.5
lodash:
specifier: ^4.18.0
version: 4.18.1
@@ -3299,8 +3299,8 @@ importers:
specifier: ^3.13.1
version: 3.13.1
liquidjs:
- specifier: ^10.25.0
- version: 10.25.0
+ specifier: ^10.25.5
+ version: 10.25.5
pluralize:
specifier: ^8.0.0
version: 8.0.0
@@ -3763,7 +3763,7 @@ importers:
version: 8.2.1
mailtrap:
specifier: ^3.1.1
- version: 3.2.0(@types/nodemailer@8.0.0)(nodemailer@8.0.4)
+ version: 3.2.0(@types/nodemailer@8.0.0)(nodemailer@8.0.5)
messagebird:
specifier: ^4.0.1
version: 4.0.1
@@ -3777,8 +3777,8 @@ importers:
specifier: ^6.0.8
version: 6.0.8
nodemailer:
- specifier: ^8.0.4
- version: 8.0.4
+ specifier: ^8.0.5
+ version: 8.0.5
plivo:
specifier: ^4.70.0
version: 4.70.0
@@ -20380,8 +20380,8 @@ packages:
resolution: {integrity: sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==}
hasBin: true
- liquidjs@10.25.0:
- resolution: {integrity: sha512-XpO7AiGULTG4xcTlwkcTI5JreFG7b6esLCLp+aUSh7YuQErJZEoUXre9u9rbdb0057pfWG4l0VursvLd5Q/eAw==}
+ liquidjs@10.25.5:
+ resolution: {integrity: sha512-GKiKeZjJDdVoQAu+S9rzkYsYnYhcep5W3WwZXgb5f+yq484P/k9JqamBbGYu+LBEixcUAXZr2jogdAIjB3ki1w==}
engines: {node: '>=16'}
hasBin: true
@@ -21729,8 +21729,8 @@ packages:
nodemailer-shared@1.1.0:
resolution: {integrity: sha512-68xW5LSyPWv8R0GLm6veAvm7E+XFXkVgvE3FW0FGxNMMZqMkPFeGDVALfR1DPdSfcoO36PnW7q5AAOgFImEZGg==}
- nodemailer@8.0.4:
- resolution: {integrity: sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==}
+ nodemailer@8.0.5:
+ resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==}
engines: {node: '>=6.0.0'}
nodemon@3.0.1:
@@ -50003,7 +50003,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- liquidjs@10.25.0:
+ liquidjs@10.25.5:
dependencies:
commander: 10.0.1
@@ -50369,12 +50369,12 @@ snapshots:
mimelib: 0.3.1
uue: 3.1.2
- mailtrap@3.2.0(@types/nodemailer@8.0.0)(nodemailer@8.0.4):
+ mailtrap@3.2.0(@types/nodemailer@8.0.0)(nodemailer@8.0.5):
dependencies:
axios: 1.13.6
optionalDependencies:
'@types/nodemailer': 8.0.0
- nodemailer: 8.0.4
+ nodemailer: 8.0.5
transitivePeerDependencies:
- debug
@@ -51768,7 +51768,7 @@ snapshots:
dependencies:
nodemailer-fetch: 1.6.0
- nodemailer@8.0.4: {}
+ nodemailer@8.0.5: {}
nodemon@3.0.1:
dependencies:
From da09d91786c2dad356dab4866a87658b69557eb4 Mon Sep 17 00:00:00 2001
From: Dima Grossman
Date: Thu, 9 Apr 2026 11:36:53 +0300
Subject: [PATCH 2/8] Update NPM package links in README.md
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index ee7e719ea04..2d437b10a6d 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,9 @@
>
-
-
+
From 3e795eaea3e5148caceb87efda100416d71f7277 Mon Sep 17 00:00:00 2001
From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:36:21 +0200
Subject: [PATCH 3/8] chore(root): remove 0 unused files and 5 unused exports
(#10621)
Co-authored-by: Cursor Agent
Co-authored-by: Adam Chmara
---
apps/api/src/utils/payload-sanitizer.ts | 14 --------------
apps/dashboard/src/components/variable/utils.ts | 2 +-
apps/dashboard/src/utils/conditions.ts | 2 +-
apps/dashboard/src/utils/constants.ts | 1 -
apps/dashboard/src/utils/logs-filters.utils.ts | 2 +-
5 files changed, 3 insertions(+), 18 deletions(-)
diff --git a/apps/api/src/utils/payload-sanitizer.ts b/apps/api/src/utils/payload-sanitizer.ts
index 00d0107a4cc..84552e7b683 100644
--- a/apps/api/src/utils/payload-sanitizer.ts
+++ b/apps/api/src/utils/payload-sanitizer.ts
@@ -16,17 +16,3 @@ export function sanitizePayload(payload: Record): string {
}
}
-export async function retryWithBackoff(fn: () => Promise, maxAttempts = 3, initialDelayMs = 100): Promise {
- let delay = initialDelayMs;
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
- try {
- return await fn();
- } catch (err) {
- if (attempt === maxAttempts - 1) throw err;
- const currentDelay = delay;
- await new Promise((resolve) => setTimeout(resolve, currentDelay));
- delay *= 2;
- }
- }
- throw new Error('Max attempts reached');
-}
diff --git a/apps/dashboard/src/components/variable/utils.ts b/apps/dashboard/src/components/variable/utils.ts
index f723e7f8494..a371a0280e8 100644
--- a/apps/dashboard/src/components/variable/utils.ts
+++ b/apps/dashboard/src/components/variable/utils.ts
@@ -57,7 +57,7 @@ export function validateEnhancedDigestFilters(filters: string[]): {
return null;
}
-export const parseParams = (input: string) => {
+const parseParams = (input: string) => {
if (!input) return '';
return input
.split(',')
diff --git a/apps/dashboard/src/utils/conditions.ts b/apps/dashboard/src/utils/conditions.ts
index 600c017715b..798e5eb0e99 100644
--- a/apps/dashboard/src/utils/conditions.ts
+++ b/apps/dashboard/src/utils/conditions.ts
@@ -132,4 +132,4 @@ export const getUniqueOperators = (jsonLogic?: RQBJsonLogic): string[] => {
};
// Export shared configuration for use in other files
-export { customJsonLogicOperations, parseJsonLogicOptions };
+export { parseJsonLogicOptions };
diff --git a/apps/dashboard/src/utils/constants.ts b/apps/dashboard/src/utils/constants.ts
index 88e7d66b69a..05a6325556f 100644
--- a/apps/dashboard/src/utils/constants.ts
+++ b/apps/dashboard/src/utils/constants.ts
@@ -61,7 +61,6 @@ export const DEFAULT_CONTROL_THROTTLE_TYPE = 'fixed';
export const DEFAULT_CONTROL_THROTTLE_WINDOW = 1;
export const DEFAULT_CONTROL_THROTTLE_UNIT = TimeUnitEnum.MINUTES;
export const DEFAULT_CONTROL_THROTTLE_THRESHOLD = 1;
-export const DEFAULT_CONTROL_THROTTLE_KEY = '';
export const DEFAULT_CONTROL_HTTP_REQUEST_METHOD = 'POST';
export const DEFAULT_CONTROL_HTTP_REQUEST_HEADERS: unknown[] = [];
diff --git a/apps/dashboard/src/utils/logs-filters.utils.ts b/apps/dashboard/src/utils/logs-filters.utils.ts
index 9ab6491aa48..c1f20d77bed 100644
--- a/apps/dashboard/src/utils/logs-filters.utils.ts
+++ b/apps/dashboard/src/utils/logs-filters.utils.ts
@@ -4,7 +4,7 @@ import { IS_SELF_HOSTED } from '../config';
type OrganizationLike = { createdAt: Date };
-export const LOGS_DATE_RANGE_OPTIONS = [
+const LOGS_DATE_RANGE_OPTIONS = [
{ value: '24', label: 'Last 24 Hours', ms: 24 * 60 * 60 * 1000 },
{ value: '168', label: '7 Days', ms: 7 * 24 * 60 * 60 * 1000 }, // 7 * 24
{ value: '720', label: '30 Days', ms: 30 * 24 * 60 * 60 * 1000 }, // 30 * 24
From a6a55400a4fbc8ccb9c128391771d3ece0c1215c Mon Sep 17 00:00:00 2001
From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com>
Date: Thu, 9 Apr 2026 12:09:53 +0200
Subject: [PATCH 4/8] chore(root): remove 0 unused files and 9 unused exports
(#10628)
Co-authored-by: Cursor Agent
Co-authored-by: Adam Chmara
---
.../utils/schema-change-detection.ts | 22 -------------------
.../src/components/variable/utils.ts | 2 +-
.../welcome/framework-guides.instructions.tsx | 11 ++++------
apps/dashboard/src/utils/context.ts | 2 +-
apps/dashboard/src/utils/schema.ts | 2 +-
5 files changed, 7 insertions(+), 32 deletions(-)
diff --git a/apps/dashboard/src/components/schema-editor/utils/schema-change-detection.ts b/apps/dashboard/src/components/schema-editor/utils/schema-change-detection.ts
index 37064ae03df..fd4fa3afaba 100644
--- a/apps/dashboard/src/components/schema-editor/utils/schema-change-detection.ts
+++ b/apps/dashboard/src/components/schema-editor/utils/schema-change-detection.ts
@@ -145,25 +145,3 @@ export function detectSchemaChanges(
return changes;
}
-
-export function getChangesSummary(changes: SchemaChanges): string {
- const parts: string[] = [];
-
- if (changes.deleted.length > 0) {
- parts.push(`${changes.deleted.length} deleted`);
- }
-
- if (changes.added.length > 0) {
- parts.push(`${changes.added.length} added`);
- }
-
- if (changes.typeChanged.length > 0) {
- parts.push(`${changes.typeChanged.length} type changed`);
- }
-
- if (changes.requiredChanged.length > 0) {
- parts.push(`${changes.requiredChanged.length} required status changed`);
- }
-
- return parts.join(', ');
-}
diff --git a/apps/dashboard/src/components/variable/utils.ts b/apps/dashboard/src/components/variable/utils.ts
index a371a0280e8..22adc89f64c 100644
--- a/apps/dashboard/src/components/variable/utils.ts
+++ b/apps/dashboard/src/components/variable/utils.ts
@@ -5,7 +5,7 @@ function escapeString(str: string): string {
return String(str).replace(/'/g, "\\'");
}
-export function formatParamValue(param: string, type?: string) {
+function formatParamValue(param: string, type?: string) {
if (type === 'number') {
return param;
}
diff --git a/apps/dashboard/src/components/welcome/framework-guides.instructions.tsx b/apps/dashboard/src/components/welcome/framework-guides.instructions.tsx
index 2f24d49b906..164fe80304c 100644
--- a/apps/dashboard/src/components/welcome/framework-guides.instructions.tsx
+++ b/apps/dashboard/src/components/welcome/framework-guides.instructions.tsx
@@ -71,7 +71,7 @@ function stepsByMethod(
return manualSteps;
}
-export const customizationTip = {
+const customizationTip = {
title: 'Tip:',
description: (
<>
@@ -89,7 +89,7 @@ export const customizationTip = {
),
};
-export const commonInstallStep = (packageName: string): InstallationStep => ({
+const commonInstallStep = (packageName: string): InstallationStep => ({
title: 'Install the package',
description: `${packageName} is the package that powers the notification center.`,
code: `npm install ${packageName}`,
@@ -97,7 +97,7 @@ export const commonInstallStep = (packageName: string): InstallationStep => ({
codeTitle: 'Terminal',
});
-export const commonCLIInstallStep = (): InstallationStep => ({
+const commonCLIInstallStep = (): InstallationStep => ({
title: 'Run the CLI command in an existing project',
description: `You'll notice a new folder in your project called inbox. This is where you'll find the inbox component boilerplate code. \n You can customize the component to match your app theme.`,
code: `npx add-inbox@latest --appId YOUR_APPLICATION_IDENTIFIER --subscriberId YOUR_SUBSCRIBER_ID${cliFlags}`,
@@ -105,7 +105,7 @@ export const commonCLIInstallStep = (): InstallationStep => ({
codeTitle: 'Terminal',
});
-export const commonAIAssistInstallStep = (
+const commonAIAssistInstallStep = (
frameworkName: string,
applicationIdentifier: string,
subscriberId: string
@@ -428,6 +428,3 @@ novu.mountComponent({
),
},
];
-
-// Export a default frameworks array for backward compatibility
-export const frameworks = getFrameworks('manual');
diff --git a/apps/dashboard/src/utils/context.ts b/apps/dashboard/src/utils/context.ts
index d6b7db85865..89bdb96ca13 100644
--- a/apps/dashboard/src/utils/context.ts
+++ b/apps/dashboard/src/utils/context.ts
@@ -1,6 +1,6 @@
import React from 'react';
-export function assertContextExists(contextVal: unknown, msgOrCtx: string | React.Context): asserts contextVal {
+function assertContextExists(contextVal: unknown, msgOrCtx: string | React.Context): asserts contextVal {
if (!contextVal) {
throw typeof msgOrCtx === 'string' ? new Error(msgOrCtx) : new Error(`${msgOrCtx.displayName} not found`);
}
diff --git a/apps/dashboard/src/utils/schema.ts b/apps/dashboard/src/utils/schema.ts
index a4616ef6c12..3c2aab45f00 100644
--- a/apps/dashboard/src/utils/schema.ts
+++ b/apps/dashboard/src/utils/schema.ts
@@ -139,7 +139,7 @@ const getZodValueByType = (jsonSchema: JSONSchemaDefinition, key: string): ZodVa
* The function will recursively build the schema based on the JSONSchema object.
* It removes empty strings and objects with empty required fields during the transformation phase after parsing.
*/
-export const buildDynamicZodSchema = (obj: JSONSchemaDefinition, key = ''): ZodValue => {
+const buildDynamicZodSchema = (obj: JSONSchemaDefinition, key = ''): ZodValue => {
if (typeof obj === 'object' && obj.type === 'object') {
const properties = obj.properties ?? {};
const requiredFields = obj.required ?? [];
From 519da21bc93277ca6faf67b54f095439c30f8c0e Mon Sep 17 00:00:00 2001
From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com>
Date: Thu, 9 Apr 2026 13:53:04 +0300
Subject: [PATCH 5/8] fix(dashboard): inflight autosave fixes NV-7294 (#10544)
---
.cursor/settings.json | 3 ++
apps/dashboard/src/hooks/use-form-autosave.ts | 9 ++--
.../src/pages/edit-step-template-v2.tsx | 47 +++++++++++++++++--
3 files changed, 51 insertions(+), 8 deletions(-)
diff --git a/.cursor/settings.json b/.cursor/settings.json
index 15e13fa632c..faa76db02ed 100644
--- a/.cursor/settings.json
+++ b/.cursor/settings.json
@@ -5,6 +5,9 @@
},
"linear": {
"enabled": true
+ },
+ "clickhouse-cursor-plugin": {
+ "enabled": true
}
}
}
diff --git a/apps/dashboard/src/hooks/use-form-autosave.ts b/apps/dashboard/src/hooks/use-form-autosave.ts
index 357619f24e8..14c9e354fa0 100644
--- a/apps/dashboard/src/hooks/use-form-autosave.ts
+++ b/apps/dashboard/src/hooks/use-form-autosave.ts
@@ -59,9 +59,12 @@ export function useFormAutosave, T extends Fie
lastSavedDataRef.current = serializedData;
save(values, {
onSuccess: () => {
- // Reset dirty state after successful save so that polling hooks (e.g. useStepResolverPolling)
- // are not permanently blocked. keepValues: true avoids regenerating useFieldArray field IDs.
- formRef.current.reset(values, { keepErrors: true, keepValues: true });
+ // Reset dirty state after successful save so polling hooks (e.g. useStepResolverPolling)
+ // are not permanently blocked. We reset with the CURRENT form values (not the stale `values`
+ // snapshot) to avoid overwriting edits the user made while the request was in-flight.
+ // keepValues:true prevents regenerating useFieldArray field IDs (row flicker).
+ const currentValues = formRef.current.getValues();
+ formRef.current.reset(currentValues, { keepErrors: true, keepValues: true });
options?.onSuccess?.();
},
});
diff --git a/apps/dashboard/src/pages/edit-step-template-v2.tsx b/apps/dashboard/src/pages/edit-step-template-v2.tsx
index 7469bba0083..250fd42fdbb 100644
--- a/apps/dashboard/src/pages/edit-step-template-v2.tsx
+++ b/apps/dashboard/src/pages/edit-step-template-v2.tsx
@@ -27,6 +27,11 @@ export function EditStepTemplateV2Page() {
const prevStepIdRef = useRef(undefined);
const prevHashRef = useRef(undefined);
const prevControlsFingerprintRef = useRef(null);
+ // Tracks ALL in-flight save fingerprints so that any server response that
+ // echoes back one of our own saves is recognized and does not reset the form.
+ // A single ref would be overwritten by rapid successive saves, causing the
+ // guard to fail for earlier in-flight requests.
+ const inFlightFingerprintsRef = useRef>(new Set());
useEffect(() => {
if (!step) return;
@@ -43,10 +48,25 @@ export function EditStepTemplateV2Page() {
const controlsChanged =
prevControlsFingerprintRef.current !== null && fingerprint !== prevControlsFingerprintRef.current;
- if (isFirstBind || stepIdChanged || hashChanged || controlsChanged) {
- prevStepIdRef.current = step.stepId;
- prevHashRef.current = step.stepResolverHash;
- prevControlsFingerprintRef.current = fingerprint;
+ // If there are any in-flight saves, any server-side change we receive is
+ // the result of our own edits. Skip the reset so we don't overwrite edits
+ // the user made while requests were in-flight. The invocationQueue may
+ // apply pending requests on top, so the server response FP may not exactly
+ // match any single in-flight FP â checking the count is safer.
+ const hasInFlightSaves = inFlightFingerprintsRef.current.size > 0;
+ const isOwnSaveEcho = controlsChanged && (inFlightFingerprintsRef.current.has(fingerprint) || hasInFlightSaves);
+
+ if (inFlightFingerprintsRef.current.has(fingerprint)) {
+ inFlightFingerprintsRef.current.delete(fingerprint);
+ }
+
+ const shouldReset = isFirstBind || stepIdChanged || hashChanged || (controlsChanged && !isOwnSaveEcho);
+
+ prevStepIdRef.current = step.stepId;
+ prevHashRef.current = step.stepResolverHash;
+ prevControlsFingerprintRef.current = fingerprint;
+
+ if (shouldReset) {
hasInitializedRef.current = true;
form.reset(getControlsDefaultValues(step), { keepErrors: true });
}
@@ -58,10 +78,27 @@ export function EditStepTemplateV2Page() {
save: (data, { onSuccess }) => {
if (!workflow || !step) return;
+ const fp = JSON.stringify({
+ v: data,
+ ui: step.controls?.uiSchema,
+ ds: step.controls?.dataSchema,
+ });
+
+ // Add to in-flight set before the request goes out. The fingerprint
+ // effect will recognize any server response that matches this value and
+ // skip the form.reset() that would otherwise overwrite in-progress edits.
+ inFlightFingerprintsRef.current.add(fp);
+
const updateStepData: Partial = {
controlValues: data,
};
- update(updateStepInWorkflow(workflow, step.stepId, updateStepData), { onSuccess });
+ update(updateStepInWorkflow(workflow, step.stepId, updateStepData), {
+ onSuccess: () => {
+ // Clean up the in-flight fingerprint on success.
+ inFlightFingerprintsRef.current.delete(fp);
+ onSuccess?.();
+ },
+ });
},
});
From a4cbc1a36e3d6150f564486b25cdaf5019d669a2 Mon Sep 17 00:00:00 2001
From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com>
Date: Thu, 9 Apr 2026 13:57:39 +0300
Subject: [PATCH 6/8] refactor(api-service): remove throttling and add metrics
(#10597)
---
.source | 2 +-
.../src/workflows/usage-report/usage-report.workflow.ts | 7 -------
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/.source b/.source
index 7a7d5cafac9..ceb439baeb8 160000
--- a/.source
+++ b/.source
@@ -1 +1 @@
-Subproject commit 7a7d5cafac90258bed07e2afee67b3a133da36bc
+Subproject commit ceb439baeb820d809af531cc16348394d4970d3f
diff --git a/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts b/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts
index 08e8f02c78a..2fc661a9a80 100644
--- a/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts
+++ b/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts
@@ -16,13 +16,6 @@ export const usageReportWorkflow = workflow(
}
);
- await step.throttle('throttle', async () => ({
- type: 'fixed',
- amount: 7,
- unit: 'days',
- threshold: 1,
- }));
-
await step.email(
'email',
async (controls) => {
From ab025332597fe558d51a3dee11cfc2c299f08019 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Tymczuk?=
Date: Thu, 9 Apr 2026 13:32:00 +0200
Subject: [PATCH 7/8] feat(dashboard,api-service): novu copilot suggestions on
the new chat fixes NV-7321 (#10542)
---
.source | 2 +-
.../get-notifications-count-request.dto.ts | 4 +-
.../delete-all-notifications.command.ts | 2 +-
.../get-notifications.command.ts | 2 +-
.../notifications-count.command.ts | 4 +-
.../update-all-notifications.command.ts | 2 +-
apps/api/src/utils/payload-sanitizer.ts | 1 -
.../ai-sidekick/ai-chat-context.tsx | 38 +-
.../src/components/ai-sidekick/chat-body.tsx | 68 ++-
.../ai-sidekick/chat-chain-of-thought.tsx | 126 ++++++
.../ai-sidekick/novu-copilot-panel.tsx | 4 +
.../analytics/utils/chart-validation.ts | 6 +-
.../dashboard/src/components/icons/code-2.tsx | 13 +-
.../layouts/layout-editor-settings-drawer.tsx | 6 +-
.../src/components/primitives/tag-input.tsx | 4 +-
.../components/property-name-input.tsx | 2 +-
.../schema-property-settings-popover.tsx | 2 +-
.../template-store/workflow-sidebar.tsx | 2 +-
.../variable/edit-variable-popover.tsx | 2 +-
.../webhooks/webhooks-empty-state-svg.tsx | 45 +-
.../workflow-editor/condition-badge.tsx | 1 -
.../workflow-editor/editor-breadcrumbs.tsx | 4 +-
.../email/configure-email-step-preview.tsx | 6 +-
.../steps/http-request/request-endpoint.tsx | 6 +-
.../steps/skip-conditions-button.tsx | 3 +-
.../steps/step-editor-layout.tsx | 66 ++-
.../workflow-editor/workflow-tabs.tsx | 59 ++-
.../dashboard/src/hooks/use-ai-chat-stream.ts | 2 +-
enterprise/packages/ai/package.json | 3 +-
.../src/factories/sms/handlers/index.ts | 2 +-
.../src/services/sqs/sqs-consumer.service.ts | 6 +-
.../sqs/sqs-payload-offload.service.ts | 10 +-
packages/js/src/event-emitter/types.ts | 2 +-
.../js/src/utils/notification-utils.test.ts | 12 +-
packages/providers/src/lib/sms/index.ts | 2 +-
.../src/consts/providers/channels/sms.ts | 2 +-
packages/shared/src/types/ai.ts | 11 +
packages/shared/src/utils/index.ts | 2 +-
packages/shared/src/utils/tags-filter.spec.ts | 23 +-
.../src/components/ai-elements/code-block.tsx | 265 ++++--------
.../components/ai-elements/conversation.tsx | 81 ++--
.../src/components/ai-elements/shimmer.tsx | 50 +--
.../src/components/ai-elements/terminal.tsx | 117 ++----
.../components/ai-elements/voice-selector.tsx | 394 +++++++-----------
.../src/components/hooks/demo/inbox-item.tsx | 5 +-
.../hooks/demo/more-actions-dropdown.tsx | 2 +-
.../components/hooks/demo/notion-theme.tsx | 12 +-
.../components/hooks/demo/status-dropdown.tsx | 2 +-
.../nextjs/src/components/hooks/icons.tsx | 1 -
.../nextjs/src/components/ui/button.tsx | 2 +-
playground/nextjs/src/components/ui/card.tsx | 142 ++-----
.../nextjs/src/pages/custom-popover/index.tsx | 2 +-
playground/nextjs/src/pages/hooks/index.tsx | 4 +-
.../nextjs/src/pages/render-bell/index.tsx | 5 +-
pnpm-lock.yaml | 10 +
55 files changed, 755 insertions(+), 896 deletions(-)
diff --git a/.source b/.source
index ceb439baeb8..07c3fdf70e9 160000
--- a/.source
+++ b/.source
@@ -1 +1 @@
-Subproject commit ceb439baeb820d809af531cc16348394d4970d3f
+Subproject commit 07c3fdf70e9361536abd25708aba1f6fb57eb4a0
diff --git a/apps/api/src/app/inbox/dtos/get-notifications-count-request.dto.ts b/apps/api/src/app/inbox/dtos/get-notifications-count-request.dto.ts
index a1fb3ab46da..d366ab1c664 100644
--- a/apps/api/src/app/inbox/dtos/get-notifications-count-request.dto.ts
+++ b/apps/api/src/app/inbox/dtos/get-notifications-count-request.dto.ts
@@ -1,10 +1,10 @@
import { BadRequestException } from '@nestjs/common';
-import { type TagsFilter, SeverityLevelEnum } from '@novu/shared';
+import { SeverityLevelEnum, type TagsFilter } from '@novu/shared';
import { plainToClass, Transform, Type } from 'class-transformer';
import { ArrayMaxSize, IsArray, IsBoolean, IsDefined, IsOptional, ValidateNested } from 'class-validator';
import { IsEnumOrArray } from '../../shared/validators/is-enum-or-array';
-import { IsTagsFilter } from '../validators/is-tags-filter.validator';
import { NotificationFilter } from '../utils/types';
+import { IsTagsFilter } from '../validators/is-tags-filter.validator';
export class NotificationsFilter implements NotificationFilter {
@IsOptional()
diff --git a/apps/api/src/app/inbox/usecases/delete-all-notifications/delete-all-notifications.command.ts b/apps/api/src/app/inbox/usecases/delete-all-notifications/delete-all-notifications.command.ts
index 5c0304a36ec..47f0c7d68cd 100644
--- a/apps/api/src/app/inbox/usecases/delete-all-notifications/delete-all-notifications.command.ts
+++ b/apps/api/src/app/inbox/usecases/delete-all-notifications/delete-all-notifications.command.ts
@@ -3,8 +3,8 @@ import { Type } from 'class-transformer';
import { IsBoolean, IsDefined, IsOptional, IsString, ValidateNested } from 'class-validator';
import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command';
-import { IsTagsFilter } from '../../validators/is-tags-filter.validator';
import { NotificationFilter } from '../../utils/types';
+import { IsTagsFilter } from '../../validators/is-tags-filter.validator';
class Filter implements NotificationFilter {
@IsOptional()
diff --git a/apps/api/src/app/inbox/usecases/get-notifications/get-notifications.command.ts b/apps/api/src/app/inbox/usecases/get-notifications/get-notifications.command.ts
index 94d022f7b75..620dcebaefd 100644
--- a/apps/api/src/app/inbox/usecases/get-notifications/get-notifications.command.ts
+++ b/apps/api/src/app/inbox/usecases/get-notifications/get-notifications.command.ts
@@ -1,4 +1,4 @@
-import { type TagsFilter, SeverityLevelEnum } from '@novu/shared';
+import { SeverityLevelEnum, type TagsFilter } from '@novu/shared';
import { IsBoolean, IsDefined, IsInt, IsMongoId, IsOptional, IsString, Max, Min } from 'class-validator';
import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command';
diff --git a/apps/api/src/app/inbox/usecases/notifications-count/notifications-count.command.ts b/apps/api/src/app/inbox/usecases/notifications-count/notifications-count.command.ts
index 16fee6eb681..40beb8ea8c3 100644
--- a/apps/api/src/app/inbox/usecases/notifications-count/notifications-count.command.ts
+++ b/apps/api/src/app/inbox/usecases/notifications-count/notifications-count.command.ts
@@ -1,10 +1,10 @@
import { SubscriberEntity } from '@novu/dal';
-import { type TagsFilter, SeverityLevelEnum } from '@novu/shared';
+import { SeverityLevelEnum, type TagsFilter } from '@novu/shared';
import { IsArray, IsBoolean, IsDefined, IsOptional } from 'class-validator';
import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command';
import { IsEnumOrArray } from '../../../shared/validators/is-enum-or-array';
-import { IsTagsFilter } from '../../validators/is-tags-filter.validator';
import { NotificationFilter } from '../../utils/types';
+import { IsTagsFilter } from '../../validators/is-tags-filter.validator';
class NotificationsFilter implements NotificationFilter {
@IsOptional()
diff --git a/apps/api/src/app/inbox/usecases/update-all-notifications/update-all-notifications.command.ts b/apps/api/src/app/inbox/usecases/update-all-notifications/update-all-notifications.command.ts
index d6735144e77..d98092df737 100644
--- a/apps/api/src/app/inbox/usecases/update-all-notifications/update-all-notifications.command.ts
+++ b/apps/api/src/app/inbox/usecases/update-all-notifications/update-all-notifications.command.ts
@@ -3,8 +3,8 @@ import { Type } from 'class-transformer';
import { IsBoolean, IsDefined, IsOptional, IsString, ValidateNested } from 'class-validator';
import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command';
-import { IsTagsFilter } from '../../validators/is-tags-filter.validator';
import { NotificationFilter } from '../../utils/types';
+import { IsTagsFilter } from '../../validators/is-tags-filter.validator';
class Filter implements NotificationFilter {
@IsOptional()
diff --git a/apps/api/src/utils/payload-sanitizer.ts b/apps/api/src/utils/payload-sanitizer.ts
index 84552e7b683..101326da6e8 100644
--- a/apps/api/src/utils/payload-sanitizer.ts
+++ b/apps/api/src/utils/payload-sanitizer.ts
@@ -15,4 +15,3 @@ export function sanitizePayload(payload: Record): string {
return '[Unserializable Payload]';
}
}
-
diff --git a/apps/dashboard/src/components/ai-sidekick/ai-chat-context.tsx b/apps/dashboard/src/components/ai-sidekick/ai-chat-context.tsx
index e5cb6cba2ab..9ef8c7f5302 100644
--- a/apps/dashboard/src/components/ai-sidekick/ai-chat-context.tsx
+++ b/apps/dashboard/src/components/ai-sidekick/ai-chat-context.tsx
@@ -1,7 +1,9 @@
import { AiAgentTypeEnum, AiMessageRoleEnum, AiResourceTypeEnum } from '@novu/shared';
import * as Sentry from '@sentry/react';
-import { ChatStatus, DataUIPart, DynamicToolUIPart, generateId, UIMessage } from 'ai';
-import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import { ChatStatus, DataUIPart, DynamicToolUIPart, UIMessage } from 'ai';
+import { createContext, FC, SVGProps, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { IconType } from 'react-icons';
import { useLocation } from 'react-router-dom';
import { cancelStream } from '@/api/ai';
import { ConfirmationModal } from '@/components/confirmation-modal';
@@ -13,6 +15,7 @@ import { useFetchLatestAiChat } from '@/hooks/use-fetch-latest-ai-chat';
import { useKeepAiChanges } from '@/hooks/use-keep-ai-changes';
import { useRevertMessage } from '@/hooks/use-revert-message';
import { useTelemetry } from '@/hooks/use-telemetry';
+import { QueryKeys } from '@/utils/query-keys';
import { TelemetryEvent } from '@/utils/telemetry';
import { showErrorToast } from '../primitives/sonner-helpers';
import { isCancelledToolCall } from './message-utils';
@@ -33,17 +36,20 @@ export type AiChatContextValue = {
isActionPending: boolean;
isReviewingChanges: boolean;
inputText: string;
+ newChatSuggestions?: { label: string; icon: IconType | FC> }[];
setInputText: (text: string) => void;
handleSendMessage: (message: string) => Promise;
handleKeepAll: () => Promise;
handleTryAgain: (messageId: string) => Promise;
handleRevertMessage: (messageId: string) => Promise;
handleDiscard: (messageId: string) => Promise;
+ handleSuggestionClick: (suggestion: string) => void;
};
export type AiChatResourceConfig = {
resourceType: AiResourceTypeEnum;
resourceId?: string;
+ newChatSuggestions?: { label: string; icon: IconType | FC> }[];
agentType: AiAgentTypeEnum;
metadata?: Record;
isResourceLoading?: boolean;
@@ -118,6 +124,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
const {
resourceType,
resourceId,
+ newChatSuggestions,
agentType,
metadata,
isResourceLoading = false,
@@ -139,8 +146,10 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
const hasHandledInitialResumeRef = useRef(false);
const isStoppingRef = useRef(false);
const skipMessageSyncRef = useRef(false);
+ const pendingSendRef = useRef<{ chatId: string; prompt: string; metadata: Record } | null>(null);
const location = useLocation();
const { areEnvironmentsInitialLoading, currentEnvironment } = useEnvironment();
+ const queryClient = useQueryClient();
const {
latestChat,
@@ -158,7 +167,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
return location.state.chatId as string;
}
- return latestChat?._id ?? generateId();
+ return latestChat?._id;
}, [location, latestChat]);
const { setMessages, sendPrompt, stop, status, isGenerating, messages, dataParts, isAborted, resume, error } =
@@ -203,6 +212,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
},
});
const dataRef = useDataRef({
+ currentEnvironment,
isGenerating,
resourceType,
resourceId,
@@ -249,6 +259,14 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
}
}, [latestChat, resume, track, dataRef]);
+ useEffect(() => {
+ if (chatId && pendingSendRef.current && chatId === pendingSendRef.current.chatId) {
+ const pending = pendingSendRef.current;
+ pendingSendRef.current = null;
+ sendPrompt({ chatId: pending.chatId, prompt: pending.prompt, metadata: pending.metadata });
+ }
+ }, [chatId, sendPrompt]);
+
useEffect(() => {
isMountedRef.current = true;
@@ -278,7 +296,8 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
const handleSendMessage = useCallback(
async (message: string) => {
- const { resourceType, resourceId, agentType, latestChat, messages, metadata } = dataRef.current;
+ const { resourceType, resourceId, agentType, latestChat, messages, metadata, currentEnvironment } =
+ dataRef.current;
const isLastUserMessage = messages.length > 0 && messages[messages.length - 1].role === AiMessageRoleEnum.USER;
const messageToSend = message.trim();
@@ -286,12 +305,16 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
if (!latestChat) {
const newChat = await createAiChat({ resourceType, resourceId });
+ queryClient.setQueryData([QueryKeys.fetchChat, currentEnvironment?._id, resourceType, resourceId], newChat);
track(TelemetryEvent.COPILOT_CHAT_CREATED, {
chatId: newChat._id,
resourceType,
agentType,
});
- sendPrompt({ chatId: newChat._id, prompt: messageToSend, metadata: { ...metadata } });
+ // we don't pre-create the chat until the user sends a message and the useAiChatStream hook uses the autogenerated chatId
+ // if we update the chatId right away here, the useAiChatStream hook will reset its state and the user sent message and stream will be lost
+ // defer sending the message to the stream until the chat is created and chatId is updated for the useAiChatStream hook
+ pendingSendRef.current = { chatId: newChat._id, prompt: messageToSend, metadata: { ...metadata } };
} else if (isLastUserMessage) {
const lastUserMessage = messages.filter((m) => m.role === AiMessageRoleEnum.USER).pop();
sendPrompt({
@@ -313,7 +336,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
setInputText('');
},
- [dataRef, createAiChat, sendPrompt, track]
+ [dataRef, queryClient, createAiChat, sendPrompt, track]
);
const handleKeepAll = useCallback(async () => {
@@ -540,12 +563,14 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
isActionPending,
isReviewingChanges,
inputText,
+ newChatSuggestions,
setInputText,
handleSendMessage,
handleKeepAll,
handleTryAgain,
handleRevertMessage,
handleDiscard,
+ handleSuggestionClick: handleSendMessage,
}),
[
hasNoChatHistory,
@@ -561,6 +586,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
isActionPending,
isReviewingChanges,
inputText,
+ newChatSuggestions,
handleSendMessage,
handleKeepAll,
handleTryAgain,
diff --git a/apps/dashboard/src/components/ai-sidekick/chat-body.tsx b/apps/dashboard/src/components/ai-sidekick/chat-body.tsx
index 0bd39e9c056..6897ec75ab4 100644
--- a/apps/dashboard/src/components/ai-sidekick/chat-body.tsx
+++ b/apps/dashboard/src/components/ai-sidekick/chat-body.tsx
@@ -1,5 +1,6 @@
import { ChatStatus, UIMessage } from 'ai';
-import { FormEvent, useMemo } from 'react';
+import { FC, FormEvent, SVGProps, useMemo } from 'react';
+import { IconType } from 'react-icons';
import { Conversation, ConversationContent, ConversationScrollButton } from '../ai-elements/conversation';
import { Message } from '../ai-elements/message';
import {
@@ -12,6 +13,7 @@ import {
} from '../ai-elements/prompt-input';
import { Broom } from '../icons/broom';
import { BroomSparkle } from '../icons/broom-sparkle';
+import { Badge, BadgeIcon } from '../primitives/badge';
import { Skeleton } from '../primitives/skeleton';
import { AssistantMessage } from './assistant-message';
import { hasKnownMessageParts } from './message-utils';
@@ -57,8 +59,51 @@ export const ChatBodySkeleton = () => {
);
};
+const ChatBodyNoHistory = ({
+ newChatSuggestions,
+ isGenerating,
+ onSuggestionClick,
+}: {
+ newChatSuggestions?: { label: string; icon: IconType | FC> }[];
+ isGenerating: boolean;
+ onSuggestionClick: (suggestion: string) => void;
+}) => {
+ return (
+
+
+
+
+
+ Novu Copilot
+
+
+
+ Suggests improvements, fills gaps, and applies best practices as you build.{' '}
+
+
+
+ {newChatSuggestions?.map((suggestion) => (
+ onSuggestionClick(suggestion.label)}
+ >
+
+ {suggestion.label}
+
+ ))}
+
+
+ );
+};
+
export const ChatBody = ({
hasNoChatHistory,
+ newChatSuggestions,
inputText,
onInputChange,
isGenerating,
@@ -75,8 +120,10 @@ export const ChatBody = ({
onDiscard,
onTryAgain,
onRevertMessage,
+ onSuggestionClick,
}: {
hasNoChatHistory: boolean;
+ newChatSuggestions?: { label: string; icon: IconType | FC> }[];
inputText: string;
onInputChange: (text: string) => void;
isGenerating: boolean;
@@ -93,6 +140,7 @@ export const ChatBody = ({
onDiscard: (messageId: string) => void;
onTryAgain: (messageId: string) => void;
onRevertMessage: (messageId: string) => void;
+ onSuggestionClick: (suggestion: string) => void;
}) => {
const hasLastUserMessage = messages.length === 0 || messages[messages.length - 1].role === 'user';
const lastMessage = messages[messages.length - 1];
@@ -119,19 +167,11 @@ export const ChatBody = ({
<>
{hasNoChatHistory && messages.length === 0 ? (
-
-
-
-
-
- Novu Copilot
-
-
-
- Suggests improvements, fills gaps, and applies best practices as you build.{' '}
-
-
-
+
) : (
+
+ {showStreaming ? (
+
+
+
+
+
+ ) : (
+ setIsDrawerOpen(true) : undefined}
+ >
+
+
+
+ Payload Schema
+
+ {addedCount > 0 && (
+
+ {addedCount} added
+
+ )}
+ {removedCount > 0 && (
+
+ {removedCount} removed
+
+ )}
+ {addedCount === 0 && removedCount === 0 && (
+
+ Modified
+
+ )}
+
+
+ )}
+
+
+ {isClickable && (
+
+ )}
+ >
+ );
+}
+
+function PayloadSchemaTool({
+ output,
+ error,
+ isStreaming,
+}: {
+ output?: PayloadSchemaOutput;
+ error?: string | null;
+ isStreaming: boolean;
+}) {
+ const hasError = !!error;
+ const status = hasError ? 'error' : isStreaming ? 'active' : 'complete';
+ const icon = hasError ? ErrorCircleIcon : isStreaming ? BroomIcon : CheckCircleIcon;
+
+ const label = isStreaming ? (
+ Updating Payload Schema
+ ) : hasError ? (
+ Failed to Update Payload Schema
+ ) : (
+
+ Updated Payload Schema
+
+ );
+
+ return (
+
+ {hasError ? (
+
+ {error}
+
+ ) : (
+
+ )}
+
+ );
+}
+
const toolNameToStreamingLabel = {
[AiWorkflowToolsEnum.ADD_STEP]: 'Drafting Workflow Step',
[AiWorkflowToolsEnum.ADD_STEP_IN_BETWEEN]: 'Drafting Workflow Step In Between',
@@ -473,6 +588,17 @@ export function ChatChainOfThought({ message }: ChatChainOfThoughtReasoningProps
);
}
+ if (tool.toolName === AiWorkflowToolsEnum.UPDATE_PAYLOAD_SCHEMA) {
+ return (
+ (tool.output)}
+ isStreaming={tool.state !== 'output-available' && tool.state !== 'output-error'}
+ error={tool.state === 'output-error' ? tool.errorText : undefined}
+ />
+ );
+ }
+
return null;
})}
diff --git a/apps/dashboard/src/components/ai-sidekick/novu-copilot-panel.tsx b/apps/dashboard/src/components/ai-sidekick/novu-copilot-panel.tsx
index 2e35f78a38f..eafa7403e08 100644
--- a/apps/dashboard/src/components/ai-sidekick/novu-copilot-panel.tsx
+++ b/apps/dashboard/src/components/ai-sidekick/novu-copilot-panel.tsx
@@ -20,12 +20,14 @@ export function NovuCopilotPanel({ hideHeader }: { hideHeader?: boolean }) {
isReviewingChanges,
inputText,
lastUserMessageId,
+ newChatSuggestions,
setInputText,
handleSendMessage,
handleKeepAll,
handleTryAgain,
handleRevertMessage,
handleDiscard,
+ handleSuggestionClick,
} = useAiChat();
return (
@@ -97,7 +99,9 @@ export function NovuCopilotPanel({ hideHeader }: { hideHeader?: boolean }) {
onDiscard={handleDiscard}
onTryAgain={handleTryAgain}
onRevertMessage={handleRevertMessage}
+ onSuggestionClick={handleSuggestionClick}
lastUserMessageId={lastUserMessageId}
+ newChatSuggestions={newChatSuggestions}
/>
)}
diff --git a/apps/dashboard/src/components/analytics/utils/chart-validation.ts b/apps/dashboard/src/components/analytics/utils/chart-validation.ts
index 473c601e7db..4633432ece6 100644
--- a/apps/dashboard/src/components/analytics/utils/chart-validation.ts
+++ b/apps/dashboard/src/components/analytics/utils/chart-validation.ts
@@ -60,11 +60,7 @@ export function createDateBasedHasDataChecker(
};
}
-function hasMinimumEntries(
- data: T[],
- hasDataForItem: (item: T) => boolean,
- minimumEntries: number = 2
-): boolean {
+function hasMinimumEntries(data: T[], hasDataForItem: (item: T) => boolean, minimumEntries: number = 2): boolean {
if (!data || data.length === 0) {
return false;
}
diff --git a/apps/dashboard/src/components/icons/code-2.tsx b/apps/dashboard/src/components/icons/code-2.tsx
index 26c2e7b42b3..cb0145bdb62 100644
--- a/apps/dashboard/src/components/icons/code-2.tsx
+++ b/apps/dashboard/src/components/icons/code-2.tsx
@@ -1,3 +1,4 @@
+/** biome-ignore-all lint/correctness/useUniqueElementIds: expected */
export const Code2: React.FC> = (props) => (