feat(dashboard): agent production environment UI and publish flow fixes NV-7429#10930
feat(dashboard): agent production environment UI and publish flow fixes NV-7429#10930
Conversation
…promotion Made-with: Cursor
Core promotion logic that copies an agent and its integration stubs from a source environment to a target environment, handles re-promotion idempotently, and cleans up unlinked integrations on removal. Made-with: Cursor
Thin adapters wiring AgentRepository and field-diff logic into the environments-v2 sync strategy interface contracts. Made-with: Cursor
…ters Wires the agent promotion flow into the environments-v2 publish pipeline via AgentDiffOperation, AgentSyncOperation, and AgentSyncStrategy. Made-with: Cursor
Registers all agent sync providers in SyncModule, adds AgentSyncStrategy to PublishEnvironmentUseCase and DiffEnvironmentUseCase, and exports SyncAgentToEnvironment from the agents USE_CASES barrel. Made-with: Cursor
…onments Prod agent integration links are managed exclusively by the promotion flow; direct add/remove operations are rejected with 403. Made-with: Cursor
- 6 E2E scenarios covering fresh promotion, idempotent re-promotion, metadata-only update, agent deletion, and prod-write guards - Fix guard select from ['type'] to ['type', 'name'] to avoid Mongoose quirk when projecting a single field named 'type' Made-with: Cursor
…S_ENABLED If the feature flag is off, execute/diff/getAvailableResourceIds all return empty results so the publish flow skips agents silently. Made-with: Cursor
- Adds 'agent' to ResourceToPublish type union - Extracts agents (resourceType === 'agent') from diff response in useResourceDependencies - Renders an Agents group in PublishModal (collapsible, selectable, RiRobot2Line icon) - Includes agent count in publish-success-modal summary text Made-with: Cursor
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds production/read-only guards to API and dashboard agent updates and dev-bridge changes; enables publishing agents in dashboard flows; refactors integration/setup UI into header/card components and introduces an Agent setup modal and production-aware empty/state UI. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (Dashboard)
participant Frontend as Frontend (Dashboard UI)
participant API as API (agents.usecase)
participant DB as Database / Integrations
User->>Frontend: Attempt to edit agent fields or dev-bridge
Frontend->>Frontend: readOnly? → if true, disable actions or show CTA
alt Frontend allows edit
Frontend->>API: PATCH /agents/:id (payload)
API->>API: compute hasReadOnlyFields, check devBridge fields
API->>API: assertNotProduction(envId, orgId, message) if violations
alt assertion passes
API->>DB: persist changes
DB-->>API: success
API-->>Frontend: 200 OK
Frontend-->>User: success
else assertion fails
API-->>Frontend: 403 Forbidden (message)
Frontend-->>User: show forbidden/error
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 6/8 reviews remaining, refill in 10 minutes.Comment |
- Set connectedAt: null explicitly on new AgentIntegration stubs - Delete orphaned stub Integration documents when source integration is unlinked - Restore IS_CONVERSATIONAL_AGENTS_ENABLED env var after E2E suite - Scope prod link lookup to the specific promoted agent in guard test Made-with: Cursor
- Set connectedAt: null explicitly on new AgentIntegration stubs - Delete orphaned stub Integration documents when source integration is unlinked - Restore IS_CONVERSATIONAL_AGENTS_ENABLED env var after E2E suite - Scope prod link lookup to the specific promoted agent in guard test Made-with: Cursor
- Guard against self-promotion (source == target environment) - Reuse existing stub integration when multiple agents share same dev integration - Only delete stub integration when no other agent links reference it - Assert prod link exists before testing DELETE guard Made-with: Cursor
…and remaining-links check Made-with: Cursor
- Hook agents into the existing publish-to-production modal with warning icons for first-time (inactive) agents and an InlineToast summary - Rewrite the publish success modal to the new Figma design with a conditional "Needs setup in production" section linking to agent details - Add production empty state for the agents list page - Disable "Add Agent" button and table action menus in production - Add first-time agent setup modal shown on opening a promoted agent - Make agent info fields (name, description) read-only in production on both frontend (canEditFields gate) and backend (assertNotProduction guard) - Move Bridge URL to its own card with click-to-edit; remains editable in production. Dev environments show a DEVELOPMENT/LOCAL toggle - Add "Viewing in production" InlineToast (soft-warning variant) above the sidebar and integrations tab with a "Switch to dev" CTA - Disable Delete menu item and hide "Add provider" in production - Improve setup guide layout spacing to match Figma proportions Made-with: Cursor
…n' into feat/agents-environment-promotion-dashboard Made-with: Cursor # Conflicts: # apps/api/src/app/agents/usecases/sync-agent-to-environment/sync-agent-to-environment.usecase.ts
✅ Deploy preview added
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/dashboard/src/pages/agent-details.tsx (1)
94-147:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset the setup dismissal per agent/environment.
setupModalDismissedis a single boolean, so once the modal is closed for one agent it stays hidden for every subsequent agent rendered by this component instance. That breaks the first-time setup flow when users navigate between agent detail pages without a full reload.Possible fix
const [setupModalDismissed, setSetupModalDismissed] = useState(false); + + useEffect(() => { + setSetupModalDismissed(false); + }, [currentEnvironment?._id, agentIdentifier]);Also applies to: 337-344
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/pages/agent-details.tsx` around lines 94 - 147, The modal dismissal is stored as a single boolean (setupModalDismissed) so closing it for one agent hides it for all; change this to a per-agent-per-environment key (e.g., a state map keyed by `${currentEnvironment?._id}:${agentIdentifier}`) and update setSetupModalDismissed to set that key only; update the showSetupModal calculation to consult the map for the current environment+agent key and ensure the key is recomputed when agentIdentifier or currentEnvironment changes (refer to setupModalDismissed, setSetupModalDismissed, agentIdentifier, currentEnvironment?._id, and showSetupModal).apps/dashboard/src/components/agents/agent-sidebar-widget.tsx (1)
301-310:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBlock status toggling in read-only environments.
agent.activecan still be toggled in production because this control usescanWriteinstead ofcanEditFields. That breaks the read-only UX contract.Suggested fix
<Switch checked={agent.active} - disabled={!canWrite || isUpdatePending} + disabled={!canEditFields || isUpdatePending} onCheckedChange={(checked) => { + if (!canEditFields) return; if (!checked) { setIsDeactivateModalOpen(true); } else { void updateAgentAsync({ active: true }); } }} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/agents/agent-sidebar-widget.tsx` around lines 301 - 310, The toggle Switch currently uses canWrite to control disabled state so users in read-only environments can still toggle agent.active; change the control to use canEditFields instead of canWrite (i.e., update the Switch disabled prop to disabled={!canEditFields || isUpdatePending}) and keep the existing onCheckedChange behavior (calling setIsDeactivateModalOpen and updateAgentAsync) so the toggle fully respects the read-only UX contract; update references in agent-sidebar-widget.tsx for Switch, agent.active, isUpdatePending, setIsDeactivateModalOpen, and updateAgentAsync.
🧹 Nitpick comments (1)
apps/dashboard/src/components/header-navigation/publish-modal.tsx (1)
106-134: ⚡ Quick winFlatten the resource-group lookup.
The workflow/layout/agent selection logic is much easier to read with a small helper or
switch, and this also keeps the code aligned with the no-nested-ternaries guideline.Suggested refactor
+ const getResourcesByType = (resourceType: 'workflow' | 'layout' | 'agent') => { + switch (resourceType) { + case 'workflow': + return workflows; + case 'layout': + return layouts; + case 'agent': + return agents; + } + }; + - const resources = resourceType === 'workflow' ? workflows : resourceType === 'layout' ? layouts : agents; + const resources = getResourcesByType(resourceType);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/header-navigation/publish-modal.tsx` around lines 106 - 134, Replace the nested ternary resource lookup in handleGroupToggle and getSelectedCount with a small helper (e.g., getResourcesForType) or a switch to return the correct array (workflows, layouts, or agents) based on the resourceType parameter; update both functions to call that helper, keep the rest of the logic (id extraction, selection/disabled checks, and calculateDependencyState call) unchanged so behavior is preserved while removing the nested ternaries for readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts`:
- Around line 45-50: The current guard only triggers when
command.devBridgeActive is true or devBridgeUrl is non-null, allowing callers to
set devBridgeActive/ devBridgeUrl to false/null in production; change the check
in update-agent.usecase.ts to detect any presence of these fields on the
incoming command (e.g., check for property existence/hasOwnProperty or
undefined) and call assertNotProduction(command.environmentId,
command.organizationId, ...) whenever command.devBridgeActive or
command.devBridgeUrl is provided (even if false/null), thereby blocking all
edits to dev-bridge settings in production; update references to
command.devBridgeActive, command.devBridgeUrl and reuse the existing
assertNotProduction helper for enforcement.
In `@apps/dashboard/src/components/agents/agent-setup-modal.tsx`:
- Line 31: The DialogContent component's className currently forces a hard
minimum width via min-w-[600px], which will cause overflow on small viewports;
update the class on DialogContent (in agent-setup-modal.tsx) to remove
min-w-[600px] and instead allow responsive sizing (e.g., keep max-w-[600px] and
add w-full or min-w-0/flex-1 as appropriate) so the dialog caps at 600px but can
shrink on narrow screens.
In `@apps/dashboard/src/components/agents/agents-list.tsx`:
- Around line 214-218: The check treats an unloaded environment as production
because currentEnvironment?.type !== EnvironmentTypeEnum.DEV is truthy when
currentEnvironment is undefined; update the isProductionEnv logic to only
evaluate the environment type after currentEnvironment is loaded (e.g., require
currentEnvironment to be non-null before comparing its type) so that the
production empty state and disabled "Add Agent" CTA (logic around showEmptyBlank
and isProductionEnv) don't flash prematurely; change references where similar
checks occur (lines handling isProductionEnv and the block returning
AgentsProductionEmptyState, and the related code between the blocks noted around
258-289) to use an explicit currentEnvironment existence check with
EnvironmentTypeEnum before deciding production state.
- Around line 258-289: The TooltipTrigger wrapping the Button should use the
asChild prop to avoid invalid DOM nesting; update the production branch where
TooltipTrigger currently wraps Button (and uses className="cursor-not-allowed")
to <TooltipTrigger asChild> so the Button (with disabled and className props) is
rendered as the trigger element; ensure the same pattern is applied to this
TooltipTrigger instance as used in permission-button.tsx and agents-table.tsx.
In `@apps/dashboard/src/components/agents/setup-guide-card.tsx`:
- Around line 17-31: The toggle button currently only updates visual state; make
it accessible by adding aria-expanded={isExpanded} to the <button> (the one that
calls setIsExpanded) and add aria-controls pointing to a stable id for the
expandable region, then give the content <div> (the container rendering
{children}) an id matching that aria-controls value; generate the stable id via
a prop or React's useId hook and reference that id in both the button and the
content div so assistive tech can announce expanded/collapsed state and the
controlled region.
In `@apps/dashboard/src/components/header-navigation/publish-success-modal.tsx`:
- Around line 104-113: The toggle button for expanding/collapsing (the button
wrapping RiContractUpDownLine/RiExpandUpDownLine and using needsSetupExpanded
and setNeedsSetupExpanded) lacks accessible semantics; update that button to
expose state and purpose by adding aria-expanded={needsSetupExpanded} and a
clear accessible name (e.g., aria-label that changes with state like "Collapse
setup details" vs "Expand setup details" or reference an id with
aria-labelledby), and ensure it points to the collapsible region via
aria-controls if that region has an id; keep the existing onClick
(setNeedsSetupExpanded) and icon swap logic but surface these attributes so
screen readers can identify and control the section.
In `@apps/dashboard/src/pages/agent-details.tsx`:
- Around line 125-147: The modal is opening before integrations load because
hasConnectedIntegration is false until agentIntegrationsQuery resolves; update
the showSetupModal condition to only evaluate once the integrations query has
finished (e.g., require agentIntegrationsQuery.isSuccess or
!agentIntegrationsQuery.isLoading) so the expression (showSetupModal) becomes:
isProductionEnv && agent != null && !agent.active &&
agentIntegrationsQuery.isSuccess && !hasConnectedIntegration &&
!setupModalDismissed; this will prevent the flash by gating the modal on
agentIntegrationsQuery's successful resolution.
---
Outside diff comments:
In `@apps/dashboard/src/components/agents/agent-sidebar-widget.tsx`:
- Around line 301-310: The toggle Switch currently uses canWrite to control
disabled state so users in read-only environments can still toggle agent.active;
change the control to use canEditFields instead of canWrite (i.e., update the
Switch disabled prop to disabled={!canEditFields || isUpdatePending}) and keep
the existing onCheckedChange behavior (calling setIsDeactivateModalOpen and
updateAgentAsync) so the toggle fully respects the read-only UX contract; update
references in agent-sidebar-widget.tsx for Switch, agent.active,
isUpdatePending, setIsDeactivateModalOpen, and updateAgentAsync.
In `@apps/dashboard/src/pages/agent-details.tsx`:
- Around line 94-147: The modal dismissal is stored as a single boolean
(setupModalDismissed) so closing it for one agent hides it for all; change this
to a per-agent-per-environment key (e.g., a state map keyed by
`${currentEnvironment?._id}:${agentIdentifier}`) and update
setSetupModalDismissed to set that key only; update the showSetupModal
calculation to consult the map for the current environment+agent key and ensure
the key is recomputed when agentIdentifier or currentEnvironment changes (refer
to setupModalDismissed, setSetupModalDismissed, agentIdentifier,
currentEnvironment?._id, and showSetupModal).
---
Nitpick comments:
In `@apps/dashboard/src/components/header-navigation/publish-modal.tsx`:
- Around line 106-134: Replace the nested ternary resource lookup in
handleGroupToggle and getSelectedCount with a small helper (e.g.,
getResourcesForType) or a switch to return the correct array (workflows,
layouts, or agents) based on the resourceType parameter; update both functions
to call that helper, keep the rest of the logic (id extraction,
selection/disabled checks, and calculateDependencyState call) unchanged so
behavior is preserved while removing the nested ternaries for readability.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8a460dec-4a65-473a-bab5-12d683bccffb
📒 Files selected for processing (19)
apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.tsapps/dashboard/src/api/environments.tsapps/dashboard/src/components/agents/agent-details-header.tsxapps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-layout.tsxapps/dashboard/src/components/agents/agent-integration-guides/resolve-agent-integration-guide.tsxapps/dashboard/src/components/agents/agent-integrations-tab.tsxapps/dashboard/src/components/agents/agent-overview-tab.tsxapps/dashboard/src/components/agents/agent-setup-guide.tsxapps/dashboard/src/components/agents/agent-setup-modal.tsxapps/dashboard/src/components/agents/agent-sidebar-widget.tsxapps/dashboard/src/components/agents/agents-list.tsxapps/dashboard/src/components/agents/agents-production-empty-state.tsxapps/dashboard/src/components/agents/agents-table.tsxapps/dashboard/src/components/agents/setup-guide-card.tsxapps/dashboard/src/components/header-navigation/publish-modal.tsxapps/dashboard/src/components/header-navigation/publish-success-modal.tsxapps/dashboard/src/components/primitives/inline-toast.tsxapps/dashboard/src/hooks/use-resource-dependencies.tsapps/dashboard/src/pages/agent-details.tsx
| export function AgentSetupModal({ isOpen, onClose, onSetupClick }: AgentSetupModalProps) { | ||
| return ( | ||
| <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> | ||
| <DialogContent hideCloseButton className="min-w-[600px] max-w-[600px] gap-0 overflow-hidden rounded-xl p-0"> |
There was a problem hiding this comment.
Drop the hard 600px minimum width.
min-w-[600px] will overflow smaller viewports and narrow browser windows. Keep 600px as a maximum instead of a minimum.
📐 Suggested fix
- <DialogContent hideCloseButton className="min-w-[600px] max-w-[600px] gap-0 overflow-hidden rounded-xl p-0">
+ <DialogContent hideCloseButton className="w-[calc(100vw-2rem)] max-w-[600px] gap-0 overflow-hidden rounded-xl p-0">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <DialogContent hideCloseButton className="min-w-[600px] max-w-[600px] gap-0 overflow-hidden rounded-xl p-0"> | |
| <DialogContent hideCloseButton className="w-[calc(100vw-2rem)] max-w-[600px] gap-0 overflow-hidden rounded-xl p-0"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/agents/agent-setup-modal.tsx` at line 31, The
DialogContent component's className currently forces a hard minimum width via
min-w-[600px], which will cause overflow on small viewports; update the class on
DialogContent (in agent-setup-modal.tsx) to remove min-w-[600px] and instead
allow responsive sizing (e.g., keep max-w-[600px] and add w-full or
min-w-0/flex-1 as appropriate) so the dialog caps at 600px but can shrink on
narrow screens.
There was a problem hiding this comment.
Intentionally kept at 600px min-width — this is a design requirement from Figma. The modal is only shown post-publish on desktop, not on mobile viewports.
There was a problem hiding this comment.
@ChmaraX, understood — thanks for the context! Since the modal is desktop-only (post-publish flow) and the 600px minimum is a deliberate Figma spec, the current implementation is perfectly fine. I'll record this for future reviews.
✏️ Learnings added
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10930
File: apps/dashboard/src/components/agents/agent-setup-modal.tsx:31-31
Timestamp: 2026-04-30T16:37:06.678Z
Learning: In `apps/dashboard/src/components/agents/agent-setup-modal.tsx`, the `AgentSetupModal` uses `min-w-[600px] max-w-[600px]` intentionally as a Figma design requirement. The modal is only shown post-publish on desktop viewports, not on mobile, so the hard 600px minimum width is correct. Do not flag `min-w-[600px]` on this component as a responsive/overflow issue.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/pages/agents-usecase-page.tsx:0-0
Timestamp: 2026-04-29T14:47:09.043Z
Learning: In `apps/dashboard/src/pages/agents-usecase-page.tsx`, the "Setup agent" `Button` (variant="secondary" mode="gradient" size="xs" trailingIcon={ChevronRight}) intentionally has no `onClick` handler. The page is explicitly bypassed in the current onboarding flow and kept as a placeholder for a future step. Do not flag the missing navigation handler as an issue.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx:64-66
Timestamp: 2026-04-29T14:56:46.315Z
Learning: In `apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx`, returning `null` when `!agent` is intentional and not a missing loading/error state. The component only mounts after `AgentsSetupPage` has confirmed `currentEnvironment` is loaded and `loadingPhase === 'ready'`, so the brief null render while the agent query resolves is invisible to the user — the surrounding `OnboardingShell` layout (title, toast, shell) is already rendered by the parent. `useQuery` with `retry: 2` handles transient failures. Do not flag this null-return as a missing loading/error state.
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request description should summarize what changed and why, list breaking changes, add screenshots for UI changes, and include a concise Mermaid diagram for non-trivial logic or architecture changes
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request title format: `type(scope): Description fixes NOV-<ticket-id>` (omit `fixes NOV-XXX` when no Linear ticket is in context). Example: `feat(dashboard): add workflow trigger button fixes NOV-123`
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/welcome/inbox-connected-guide.tsx:131-135
Timestamp: 2026-04-29T14:47:11.569Z
Learning: In the apps/dashboard codebase, `useTelemetry` is intentionally implemented as a React Query `useMutation` wrapper whose callback uses fire-and-forget semantics (it returns the `mutate` function, effectively `void`). Therefore, `await telemetry(...)` calls are intentional and should NOT be flagged as incorrect/buggy or as needing `mutateAsync`. Do not suggest changing `useTelemetry` to use `mutateAsync`; treat this behavior as an established codebase-wide pattern.
| {isProductionEnv ? ( | ||
| <Tooltip> | ||
| <TooltipTrigger className="cursor-not-allowed"> | ||
| <Button size="xs" variant="primary" className="gap-1.5" leadingIcon={RiRobot2Line} disabled> | ||
| Add Agent | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent className="max-w-60"> | ||
| {'Add agents in your development environment. '} | ||
| <a | ||
| href="https://docs.novu.co/platform/agents" | ||
| target="_blank" | ||
| rel="noreferrer noopener" | ||
| className="underline" | ||
| > | ||
| Learn More ↗ | ||
| </a> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| ) : ( | ||
| <PermissionButton | ||
| permission={PermissionsEnum.AGENT_WRITE} | ||
| size="xs" | ||
| variant="primary" | ||
| mode="gradient" | ||
| className="gap-1.5" | ||
| leadingIcon={RiRobot2Line} | ||
| onClick={() => setCreateOpen(true)} | ||
| > | ||
| Add Agent | ||
| </PermissionButton> | ||
| )} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/dashboard/src/components/primitives/tooltip.tsxRepository: novuhq/novu
Length of output: 3113
🏁 Script executed:
rg "TooltipTrigger" apps/dashboard/src --type tsx -A 2 -B 2Repository: novuhq/novu
Length of output: 83
🏁 Script executed:
rg "TooltipTrigger" apps/dashboard/src --type-add 'web:*.tsx;*.ts' -t web -A 2 -B 1Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
grep -r "TooltipTrigger" apps/dashboard/src --include="*.tsx" -A 2 -B 1Repository: novuhq/novu
Length of output: 50368
🏁 Script executed:
grep -A 5 "TooltipTrigger asChild" apps/dashboard/src/components/agents/agents-table.tsx | head -20Repository: novuhq/novu
Length of output: 495
🏁 Script executed:
grep -B 2 -A 8 "TooltipTrigger asChild" apps/dashboard/src/components/primitives/permission-button.tsx | head -30Repository: novuhq/novu
Length of output: 573
Use TooltipTrigger asChild around the button element.
The Button component is being nested directly inside TooltipTrigger without the asChild prop, which creates improper DOM nesting. This pattern is used elsewhere in the codebase (e.g., permission-button.tsx, agents-table.tsx) where asChild is applied to interactive elements wrapped by tooltip triggers.
Suggested fix
- <TooltipTrigger className="cursor-not-allowed">
- <Button size="xs" variant="primary" className="gap-1.5" leadingIcon={RiRobot2Line} disabled>
+ <TooltipTrigger asChild>
+ <Button size="xs" variant="primary" className="gap-1.5 cursor-not-allowed" leadingIcon={RiRobot2Line} disabled>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {isProductionEnv ? ( | |
| <Tooltip> | |
| <TooltipTrigger className="cursor-not-allowed"> | |
| <Button size="xs" variant="primary" className="gap-1.5" leadingIcon={RiRobot2Line} disabled> | |
| Add Agent | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent className="max-w-60"> | |
| {'Add agents in your development environment. '} | |
| <a | |
| href="https://docs.novu.co/platform/agents" | |
| target="_blank" | |
| rel="noreferrer noopener" | |
| className="underline" | |
| > | |
| Learn More ↗ | |
| </a> | |
| </TooltipContent> | |
| </Tooltip> | |
| ) : ( | |
| <PermissionButton | |
| permission={PermissionsEnum.AGENT_WRITE} | |
| size="xs" | |
| variant="primary" | |
| mode="gradient" | |
| className="gap-1.5" | |
| leadingIcon={RiRobot2Line} | |
| onClick={() => setCreateOpen(true)} | |
| > | |
| Add Agent | |
| </PermissionButton> | |
| )} | |
| {isProductionEnv ? ( | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button size="xs" variant="primary" className="gap-1.5 cursor-not-allowed" leadingIcon={RiRobot2Line} disabled> | |
| Add Agent | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent className="max-w-60"> | |
| {'Add agents in your development environment. '} | |
| <a | |
| href="https://docs.novu.co/platform/agents" | |
| target="_blank" | |
| rel="noreferrer noopener" | |
| className="underline" | |
| > | |
| Learn More ↗ | |
| </a> | |
| </TooltipContent> | |
| </Tooltip> | |
| ) : ( | |
| <PermissionButton | |
| permission={PermissionsEnum.AGENT_WRITE} | |
| size="xs" | |
| variant="primary" | |
| mode="gradient" | |
| className="gap-1.5" | |
| leadingIcon={RiRobot2Line} | |
| onClick={() => setCreateOpen(true)} | |
| > | |
| Add Agent | |
| </PermissionButton> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/agents/agents-list.tsx` around lines 258 - 289,
The TooltipTrigger wrapping the Button should use the asChild prop to avoid
invalid DOM nesting; update the production branch where TooltipTrigger currently
wraps Button (and uses className="cursor-not-allowed") to <TooltipTrigger
asChild> so the Button (with disabled and className props) is rendered as the
trigger element; ensure the same pattern is applied to this TooltipTrigger
instance as used in permission-button.tsx and agents-table.tsx.
There was a problem hiding this comment.
This is the existing TooltipTrigger pattern used across the dashboard (same approach as CreateLayoutButton for disabled workflow/layout buttons). No change needed here.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
grep -r "cursor-not-allowed" apps/dashboard/src --include="*.tsx" -B 2 -A 2 | grep -A 4 -B 4 "TooltipTrigger" | head -60
Length of output: 2521
@ChmaraX, confirmed — create-layout-btn.tsx uses the exact same <TooltipTrigger className="cursor-not-allowed"> pattern (without asChild) in multiple places. My suggestion was incorrect. Thanks for the clarification!
✏️ Learnings added
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10930
File: apps/dashboard/src/components/agents/agents-list.tsx:259-290
Timestamp: 2026-04-30T16:37:12.078Z
Learning: In `apps/dashboard/src`, the established pattern for showing a tooltip over a disabled button is `<TooltipTrigger className="cursor-not-allowed"><Button ... disabled>...</Button></TooltipTrigger>` — **without** the `asChild` prop on `TooltipTrigger`. This is used in `apps/dashboard/src/components/layouts/create-layout-btn.tsx` and `apps/dashboard/src/components/agents/agents-list.tsx`. Do not flag the absence of `asChild` on `TooltipTrigger` wrapping a disabled `Button` as an issue.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/pages/agents-usecase-page.tsx:0-0
Timestamp: 2026-04-29T14:47:09.043Z
Learning: In `apps/dashboard/src/pages/agents-usecase-page.tsx`, the "Setup agent" `Button` (variant="secondary" mode="gradient" size="xs" trailingIcon={ChevronRight}) intentionally has no `onClick` handler. The page is explicitly bypassed in the current onboarding flow and kept as a placeholder for a future step. Do not flag the missing navigation handler as an issue.
Learnt from: scopsy
Repo: novuhq/novu PR: 10679
File: apps/api/src/app/agents/agents.controller.ts:77-304
Timestamp: 2026-04-13T10:55:57.245Z
Learning: In `apps/api/src/app/agents/agents.controller.ts`, the absence of the `ExternalApiAccessible` decorator on all Agents controller endpoints (createAgent, listAgents, addAgentIntegration, listAgentIntegrations, updateAgentIntegration, removeAgentIntegration, getAgent, updateAgent, deleteAgent) is **intentional**. The Agents API is deliberately kept internal (not accessible via user API keys or the official SDK) for the time being, as it is an early-access/gated feature.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx:64-66
Timestamp: 2026-04-29T14:56:46.315Z
Learning: In `apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx`, returning `null` when `!agent` is intentional and not a missing loading/error state. The component only mounts after `AgentsSetupPage` has confirmed `currentEnvironment` is loaded and `loadingPhase === 'ready'`, so the brief null render while the agent query resolves is invisible to the user — the surrounding `OnboardingShell` layout (title, toast, shell) is already rendered by the parent. `useQuery` with `retry: 2` handles transient failures. Do not flag this null-return as a missing loading/error state.
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/context-engineering.mdc:0-0
Timestamp: 2026-03-24T13:17:45.392Z
Learning: Applies to **/{tools,prompts}/**/*.ts|**/*.prompt.ts|AGENTS.md|.cursor/{rules,skills}/**|.agents/skills/** : Avoid defensive repetition with emphasis markers - state rules once without repeated 'MUST', 'NEVER', or 'CRITICAL' qualifiers
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/context-engineering.mdc:0-0
Timestamp: 2026-03-24T13:17:45.392Z
Learning: Applies to **/{tools,prompts}/**/*.ts|**/*.prompt.ts|AGENTS.md|.cursor/{rules,skills}/**|.agents/skills/** : Use specific heuristics and principles rather than hardcoded logic or vague guidance in agent instructions
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/context-engineering.mdc:0-0
Timestamp: 2026-03-24T13:17:45.392Z
Learning: Applies to **/{tools,prompts}/**/*.ts|**/*.prompt.ts|AGENTS.md|.cursor/{rules,skills}/**|.agents/skills/** : Remove context that a senior developer would obviously infer to avoid unnecessary verbosity
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2026-03-24T13:18:02.433Z
Learning: Applies to apps/dashboard/src/**/*.{ts,tsx} : Use Tailwind utility classes for all styling; avoid inline `style` props except for dynamic values that cannot be expressed as utilities
Learnt from: LetItRock
Repo: novuhq/novu PR: 10439
File: apps/dashboard/src/components/ai-sidekick/chat-body.tsx:199-207
Timestamp: 2026-03-23T15:20:40.461Z
Learning: In `apps/dashboard/src/components/ai-sidekick/chat-body.tsx`, the `onKeyDown` handler on `PromptInputTextarea` that intercepts the `Enter` key and calls `e.preventDefault()` + `onSubmit(inputText)` directly is intentional. The chat panel is embedded inside a parent host form (the step editor form in `step-editor-layout.tsx`). Without this handler, pressing Enter bubbles the keydown event up to the parent form and causes a page refresh. Stopping propagation on the form's own `onSubmit` event does not prevent the keydown event from bubbling, so the workaround must live in the textarea's `onKeyDown`.
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request title format: `type(scope): Description fixes NOV-<ticket-id>` (omit `fixes NOV-XXX` when no Linear ticket is in context). Example: `feat(dashboard): add workflow trigger button fixes NOV-123`
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/welcome/inbox-connected-guide.tsx:131-135
Timestamp: 2026-04-29T14:47:11.569Z
Learning: In the apps/dashboard codebase, `useTelemetry` is intentionally implemented as a React Query `useMutation` wrapper whose callback uses fire-and-forget semantics (it returns the `mutate` function, effectively `void`). Therefore, `await telemetry(...)` calls are intentional and should NOT be flagged as incorrect/buggy or as needing `mutateAsync`. Do not suggest changing `useTelemetry` to use `mutateAsync`; treat this behavior as an established codebase-wide pattern.
| <button | ||
| type="button" | ||
| className="flex w-full items-center justify-between px-2 py-1.5" | ||
| onClick={() => setIsExpanded((prev) => !prev)} | ||
| > | ||
| <span className="font-code text-text-sub text-[12px] uppercase leading-4 tracking-[-0.24px]">{label}</span> | ||
| <div className="flex items-center gap-2"> | ||
| {rightContent} | ||
| <RiExpandUpDownLine className={cn('text-text-soft size-3 transition-transform', isExpanded && 'rotate-180')} /> | ||
| </div> | ||
| </button> | ||
| {isExpanded && ( | ||
| <div className="bg-bg-white flex flex-col overflow-hidden rounded-md p-3 pr-6 shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]"> | ||
| {children} | ||
| </div> |
There was a problem hiding this comment.
Expose the collapsed state to assistive tech.
The chevron rotation is visual only; the toggle never announces whether the section is expanded or which region it controls. Add aria-expanded and a stable control id so the collapse state is readable.
♿ Suggested fix
-import { useState } from 'react';
+import { useId, useState } from 'react';
@@
export function SetupGuideCard({ label, rightContent, children, className }: SetupGuideCardProps) {
const [isExpanded, setIsExpanded] = useState(true);
+ const contentId = useId();
@@
<button
type="button"
className="flex w-full items-center justify-between px-2 py-1.5"
+ aria-expanded={isExpanded}
+ aria-controls={contentId}
onClick={() => setIsExpanded((prev) => !prev)}
>
@@
- <div className="bg-bg-white flex flex-col overflow-hidden rounded-md p-3 pr-6 shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]">
+ <div
+ id={contentId}
+ className="bg-bg-white flex flex-col overflow-hidden rounded-md p-3 pr-6 shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]"
+ >
{children}
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| type="button" | |
| className="flex w-full items-center justify-between px-2 py-1.5" | |
| onClick={() => setIsExpanded((prev) => !prev)} | |
| > | |
| <span className="font-code text-text-sub text-[12px] uppercase leading-4 tracking-[-0.24px]">{label}</span> | |
| <div className="flex items-center gap-2"> | |
| {rightContent} | |
| <RiExpandUpDownLine className={cn('text-text-soft size-3 transition-transform', isExpanded && 'rotate-180')} /> | |
| </div> | |
| </button> | |
| {isExpanded && ( | |
| <div className="bg-bg-white flex flex-col overflow-hidden rounded-md p-3 pr-6 shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]"> | |
| {children} | |
| </div> | |
| import { useId, useState } from 'react'; | |
| export function SetupGuideCard({ label, rightContent, children, className }: SetupGuideCardProps) { | |
| const [isExpanded, setIsExpanded] = useState(true); | |
| const contentId = useId(); | |
| return ( | |
| <div className={cn('flex flex-col gap-3', className)}> | |
| <button | |
| type="button" | |
| className="flex w-full items-center justify-between px-2 py-1.5" | |
| aria-expanded={isExpanded} | |
| aria-controls={contentId} | |
| onClick={() => setIsExpanded((prev) => !prev)} | |
| > | |
| <span className="font-code text-text-sub text-[12px] uppercase leading-4 tracking-[-0.24px]">{label}</span> | |
| <div className="flex items-center gap-2"> | |
| {rightContent} | |
| <RiExpandUpDownLine className={cn('text-text-soft size-3 transition-transform', isExpanded && 'rotate-180')} /> | |
| </div> | |
| </button> | |
| {isExpanded && ( | |
| <div | |
| id={contentId} | |
| className="bg-bg-white flex flex-col overflow-hidden rounded-md p-3 pr-6 shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]" | |
| > | |
| {children} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/agents/setup-guide-card.tsx` around lines 17 -
31, The toggle button currently only updates visual state; make it accessible by
adding aria-expanded={isExpanded} to the <button> (the one that calls
setIsExpanded) and add aria-controls pointing to a stable id for the expandable
region, then give the content <div> (the container rendering {children}) an id
matching that aria-controls value; generate the stable id via a prop or React's
useId hook and reference that id in both the button and the content div so
assistive tech can announce expanded/collapsed state and the controlled region.
There was a problem hiding this comment.
Noted — will add accessible semantics (aria-expanded, aria-controls) as a follow-up. Keeping this PR focused on the production environment functionality.
There was a problem hiding this comment.
@ChmaraX, understood — keeping the scope focused makes sense. Would you like me to open a follow-up GitHub issue to track the accessibility improvements (aria-expanded, aria-controls, and the useId wiring) for SetupGuideCard so it doesn't get lost?
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/pages/agents-usecase-page.tsx:0-0
Timestamp: 2026-04-29T14:47:09.043Z
Learning: In `apps/dashboard/src/pages/agents-usecase-page.tsx`, the "Setup agent" `Button` (variant="secondary" mode="gradient" size="xs" trailingIcon={ChevronRight}) intentionally has no `onClick` handler. The page is explicitly bypassed in the current onboarding flow and kept as a placeholder for a future step. Do not flag the missing navigation handler as an issue.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx:64-66
Timestamp: 2026-04-29T14:56:46.315Z
Learning: In `apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx`, returning `null` when `!agent` is intentional and not a missing loading/error state. The component only mounts after `AgentsSetupPage` has confirmed `currentEnvironment` is loaded and `loadingPhase === 'ready'`, so the brief null render while the agent query resolves is invisible to the user — the surrounding `OnboardingShell` layout (title, toast, shell) is already rendered by the parent. `useQuery` with `retry: 2` handles transient failures. Do not flag this null-return as a missing loading/error state.
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2026-03-24T13:18:02.433Z
Learning: Applies to apps/dashboard/src/components/**/*.{ts,tsx} : Build UI components on Radix UI primitives and the existing shadcn/ui component set in `src/components/ui/`
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request description should summarize what changed and why, list breaking changes, add screenshots for UI changes, and include a concise Mermaid diagram for non-trivial logic or architecture changes
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request title format: `type(scope): Description fixes NOV-<ticket-id>` (omit `fixes NOV-XXX` when no Linear ticket is in context). Example: `feat(dashboard): add workflow trigger button fixes NOV-123`
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/welcome/inbox-connected-guide.tsx:131-135
Timestamp: 2026-04-29T14:47:11.569Z
Learning: In the apps/dashboard codebase, `useTelemetry` is intentionally implemented as a React Query `useMutation` wrapper whose callback uses fire-and-forget semantics (it returns the `mutate` function, effectively `void`). Therefore, `await telemetry(...)` calls are intentional and should NOT be flagged as incorrect/buggy or as needing `mutateAsync`. Do not suggest changing `useTelemetry` to use `mutateAsync`; treat this behavior as an established codebase-wide pattern.
| <button | ||
| onClick={() => setNeedsSetupExpanded(!needsSetupExpanded)} | ||
| className="flex h-4 w-4 items-center justify-center rounded-lg p-0.5" | ||
| > | ||
| {needsSetupExpanded ? ( | ||
| <RiContractUpDownLine className="h-3 w-3 text-text-sub" /> | ||
| ) : ( | ||
| <RiExpandUpDownLine className="h-3 w-3 text-text-sub" /> | ||
| )} | ||
| </button> |
There was a problem hiding this comment.
Add accessible semantics to the collapse/expand toggle.
The icon-only button lacks an accessible label and state, so assistive tech users can’t identify/control the section reliably.
Suggested fix
- <button
+ <button
+ type="button"
+ aria-label={needsSetupExpanded ? 'Collapse needs setup list' : 'Expand needs setup list'}
+ aria-expanded={needsSetupExpanded}
+ aria-controls="needs-setup-list"
onClick={() => setNeedsSetupExpanded(!needsSetupExpanded)}
className="flex h-4 w-4 items-center justify-center rounded-lg p-0.5"
>
@@
- <CollapsibleContent>
+ <CollapsibleContent id="needs-setup-list">As per coding guidelines, apps/dashboard/**: “Review with focus on UX, accessibility, and performance.”
Also applies to: 117-119
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/header-navigation/publish-success-modal.tsx`
around lines 104 - 113, The toggle button for expanding/collapsing (the button
wrapping RiContractUpDownLine/RiExpandUpDownLine and using needsSetupExpanded
and setNeedsSetupExpanded) lacks accessible semantics; update that button to
expose state and purpose by adding aria-expanded={needsSetupExpanded} and a
clear accessible name (e.g., aria-label that changes with state like "Collapse
setup details" vs "Expand setup details" or reference an id with
aria-labelledby), and ensure it points to the collapsible region via
aria-controls if that region has an id; keep the existing onClick
(setNeedsSetupExpanded) and icon swap logic but surface these attributes so
screen readers can identify and control the section.
There was a problem hiding this comment.
Noted — will add accessible semantics as a follow-up. Keeping this PR focused on the production environment functionality.
There was a problem hiding this comment.
@ChmaraX, understood — happy to keep this PR focused on the production environment flow. Would you like me to open a follow-up GitHub issue to track the accessible semantics improvements for the collapse/expand toggle (aria-expanded, aria-label, aria-controls)?
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx:64-66
Timestamp: 2026-04-29T14:56:46.315Z
Learning: In `apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx`, returning `null` when `!agent` is intentional and not a missing loading/error state. The component only mounts after `AgentsSetupPage` has confirmed `currentEnvironment` is loaded and `loadingPhase === 'ready'`, so the brief null render while the agent query resolves is invisible to the user — the surrounding `OnboardingShell` layout (title, toast, shell) is already rendered by the parent. `useQuery` with `retry: 2` handles transient failures. Do not flag this null-return as a missing loading/error state.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/pages/agents-usecase-page.tsx:0-0
Timestamp: 2026-04-29T14:47:09.043Z
Learning: In `apps/dashboard/src/pages/agents-usecase-page.tsx`, the "Setup agent" `Button` (variant="secondary" mode="gradient" size="xs" trailingIcon={ChevronRight}) intentionally has no `onClick` handler. The page is explicitly bypassed in the current onboarding flow and kept as a placeholder for a future step. Do not flag the missing navigation handler as an issue.
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request description should summarize what changed and why, list breaking changes, add screenshots for UI changes, and include a concise Mermaid diagram for non-trivial logic or architecture changes
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request title format: `type(scope): Description fixes NOV-<ticket-id>` (omit `fixes NOV-XXX` when no Linear ticket is in context). Example: `feat(dashboard): add workflow trigger button fixes NOV-123`
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/welcome/inbox-connected-guide.tsx:131-135
Timestamp: 2026-04-29T14:47:11.569Z
Learning: In the apps/dashboard codebase, `useTelemetry` is intentionally implemented as a React Query `useMutation` wrapper whose callback uses fire-and-forget semantics (it returns the `mutate` function, effectively `void`). Therefore, `await telemetry(...)` calls are intentional and should NOT be flagged as incorrect/buggy or as needing `mutateAsync`. Do not suggest changing `useTelemetry` to use `mutateAsync`; treat this behavior as an established codebase-wide pattern.
TypeScript strict mode rejects null for string | undefined fields. Made-with: Cursor
- Remove unused isLocalTunnelActive variable - Block all dev-bridge mutations (not just activation) in production - Guard isProductionEnv against unloaded environment context - Wait for integrations query before showing setup modal Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/dashboard/src/components/agents/agent-sidebar-widget.tsx`:
- Around line 275-283: The switch-to-dev CTA currently navigates using the raw
agent.identifier which can break routes for characters like '/', '?', or '#';
update the onCtaClick handler so it passes an encoded identifier to buildRoute
(e.g., use encodeURIComponent on agent.identifier) when building the
ROUTES.AGENT_DETAILS route, ensuring the same encoding approach used elsewhere
in agent-detail navigations is applied.
- Around line 65-83: persistBridgeUrl currently uses "trimmed || undefined"
which omits the bridgeUrl field when the user clears it, preventing UpdateAgent
from persisting an explicit clear; change the payload so bridgeUrl is always
present (use an explicit empty string when trimmed is empty) and call onUpdate
with that value — i.e., in persistBridgeUrl replace the "trimmed || undefined"
behavior with logic that sends "" when trimmed === "" and the trimmed value
otherwise (references: persistBridgeUrl function, onUpdate call,
agent.bridgeUrl, bridgeUrl, canWrite).
In `@apps/dashboard/src/components/agents/agents-list.tsx`:
- Around line 214-215: The create dialog can remain open after switching to
production/read-only because isProductionEnv change isn't resetting createOpen;
update the component to close the create flow when the environment becomes
production/read-only by watching isProductionEnv (or
currentEnvironment/readOnly) and setting createOpen=false. Specifically, add an
effect that subscribes to changes in isProductionEnv (or currentEnvironment and
readOnly) and calls the setter for createOpen to false, ensuring
CreateAgentDialog cannot remain open or submit once the environment flips to
production/read-only.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 96568daf-77fb-4691-a184-3839b68a155e
📒 Files selected for processing (4)
apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.tsapps/dashboard/src/components/agents/agent-sidebar-widget.tsxapps/dashboard/src/components/agents/agents-list.tsxapps/dashboard/src/pages/agent-details.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/dashboard/src/pages/agent-details.tsx
| const isProductionEnv = | ||
| Boolean(currentEnvironment) && (readOnly || currentEnvironment?.type !== EnvironmentTypeEnum.DEV); |
There was a problem hiding this comment.
Close the create flow when the page becomes production/read-only.
isProductionEnv now flips correctly at runtime, but createOpen is never reset. If the user opens CreateAgentDialog in development and then switches environments, the dialog rendered below stays open and can still submit in production.
Suggested fix
const isProductionEnv =
Boolean(currentEnvironment) && (readOnly || currentEnvironment?.type !== EnvironmentTypeEnum.DEV);
+ useEffect(() => {
+ if (isProductionEnv) {
+ setCreateOpen(false);
+ }
+ }, [isProductionEnv]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/agents/agents-list.tsx` around lines 214 - 215,
The create dialog can remain open after switching to production/read-only
because isProductionEnv change isn't resetting createOpen; update the component
to close the create flow when the environment becomes production/read-only by
watching isProductionEnv (or currentEnvironment/readOnly) and setting
createOpen=false. Specifically, add an effect that subscribes to changes in
isProductionEnv (or currentEnvironment and readOnly) and calls the setter for
createOpen to false, ensuring CreateAgentDialog cannot remain open or submit
once the environment flips to production/read-only.
There was a problem hiding this comment.
Declining — environment switching triggers a full page navigation which remounts the component tree (and resets createOpen). Additionally, the backend's production guard already rejects agent creation in non-dev environments, so even if the dialog were somehow open, the API call would fail with 403.
There was a problem hiding this comment.
@ChmaraX, understood — both defenses are solid. Since environment switching causes a full page navigation and component remount, createOpen resets automatically; and the backend's production guard independently rejects the mutation with a 403 regardless. I'll retract the suggestion.
✏️ Learnings added
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10930
File: apps/dashboard/src/components/agents/agents-list.tsx:214-215
Timestamp: 2026-04-30T16:46:53.872Z
Learning: In `apps/dashboard/src/components/agents/agents-list.tsx`, the `createOpen` state for `CreateAgentDialog` does NOT need to be explicitly reset when `isProductionEnv` changes. Environment switching in the Novu dashboard triggers a full page navigation, which remounts the component tree and resets all local state including `createOpen`. Additionally, the backend production guard returns a 403 for agent creation in non-dev environments, making any defensive UI reset doubly redundant. Do not flag the absence of a `useEffect(() => { if (isProductionEnv) setCreateOpen(false); }, [isProductionEnv])` guard in this component.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx:64-66
Timestamp: 2026-04-29T14:56:46.315Z
Learning: In `apps/dashboard/src/components/onboarding/onboarding-setup-guide.tsx`, returning `null` when `!agent` is intentional and not a missing loading/error state. The component only mounts after `AgentsSetupPage` has confirmed `currentEnvironment` is loaded and `loadingPhase === 'ready'`, so the brief null render while the agent query resolves is invisible to the user — the surrounding `OnboardingShell` layout (title, toast, shell) is already rendered by the parent. `useQuery` with `retry: 2` handles transient failures. Do not flag this null-return as a missing loading/error state.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/pages/agents-usecase-page.tsx:0-0
Timestamp: 2026-04-29T14:47:09.043Z
Learning: In `apps/dashboard/src/pages/agents-usecase-page.tsx`, the "Setup agent" `Button` (variant="secondary" mode="gradient" size="xs" trailingIcon={ChevronRight}) intentionally has no `onClick` handler. The page is explicitly bypassed in the current onboarding flow and kept as a placeholder for a future step. Do not flag the missing navigation handler as an issue.
Learnt from: scopsy
Repo: novuhq/novu PR: 10679
File: apps/api/src/app/agents/agents.controller.ts:77-304
Timestamp: 2026-04-13T10:55:57.245Z
Learning: In `apps/api/src/app/agents/agents.controller.ts`, the absence of the `ExternalApiAccessible` decorator on all Agents controller endpoints (createAgent, listAgents, addAgentIntegration, listAgentIntegrations, updateAgentIntegration, removeAgentIntegration, getAgent, updateAgent, deleteAgent) is **intentional**. The Agents API is deliberately kept internal (not accessible via user API keys or the official SDK) for the time being, as it is an early-access/gated feature.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10930
File: apps/dashboard/src/components/agents/agent-setup-modal.tsx:31-31
Timestamp: 2026-04-30T16:37:06.678Z
Learning: In `apps/dashboard/src/components/agents/agent-setup-modal.tsx`, the `AgentSetupModal` uses `min-w-[600px] max-w-[600px]` intentionally as a Figma design requirement. The modal is only shown post-publish on desktop viewports, not on mobile, so the hard 600px minimum width is correct. Do not flag `min-w-[600px]` on this component as a responsive/overflow issue.
Learnt from: LetItRock
Repo: novuhq/novu PR: 10483
File: apps/dashboard/src/components/workflow-editor/steps/step-editor-layout.tsx:64-65
Timestamp: 2026-03-26T11:49:23.979Z
Learning: In `apps/dashboard/src/components/workflow-editor/steps/step-editor-layout.tsx`, the Copilot panel (`showCopilot`) is intentionally shown even for resolver-backed steps (those with `step.stepResolverHash`). Editing protection for such steps is enforced by the AI agent itself, not by hiding the Copilot UI. Do not suggest hiding the Copilot tab based on `stepResolverHash`.
Learnt from: CR
Repo: novuhq/novu PR: 0
File: .cursor/rules/pullrequest.mdc:0-0
Timestamp: 2026-03-24T13:18:35.551Z
Learning: Pull request title format: `type(scope): Description fixes NOV-<ticket-id>` (omit `fixes NOV-XXX` when no Linear ticket is in context). Example: `feat(dashboard): add workflow trigger button fixes NOV-123`
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/welcome/inbox-connected-guide.tsx:131-135
Timestamp: 2026-04-29T14:47:11.569Z
Learning: In the apps/dashboard codebase, `useTelemetry` is intentionally implemented as a React Query `useMutation` wrapper whose callback uses fire-and-forget semantics (it returns the `mutate` function, effectively `void`). Therefore, `await telemetry(...)` calls are intentional and should NOT be flagged as incorrect/buggy or as needing `mutateAsync`. Do not suggest changing `useTelemetry` to use `mutateAsync`; treat this behavior as an established codebase-wide pattern.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10930
File: apps/dashboard/src/components/agents/agents-list.tsx:259-290
Timestamp: 2026-04-30T16:37:12.078Z
Learning: In apps/dashboard/src, follow the established tooltip pattern for disabled buttons: render the disabled <Button ... disabled> inside <TooltipTrigger className="cursor-not-allowed">...</TooltipTrigger> and do not require or flag the absence of the `asChild` prop on `TooltipTrigger` when it wraps a disabled `Button`. This convention should be treated as acceptable and not reported as an issue during code review.
- Send empty string instead of undefined when clearing bridgeUrl - Encode agent identifier in the switch-to-dev navigation route Made-with: Cursor
Summary
canEditFieldsgate) and backend (assertNotProductionguard that returns 403)soft-warningvariant) above the sidebar and integrations tab with a "Switch to dev" CTAFixes NV-7429
Test plan
What changed
Agents are now first-class publishable resources and integrated into the publish-to-production flow: the publish modal lets you add agents (showing warnings for first-time/inactive promotions), and the publish-success modal shows a redesigned “Needs setup in production” checklist linking to agent pages. Newly promoted agents trigger a first-time setup modal. Production environments are read-only for agent metadata (frontend gating and a backend 403 guard). The dashboard also adds a production empty state for agents, refactors the Bridge URL into its own editable card (env-aware), and improves integration guide/layout components.
Affected areas
assertNotProduction(environmentId, organizationId, message)and applied it to agent updates; dev-bridge mutations are blocked in production whendevBridgeActiveordevBridgeUrlare provided.ResourceToPublishand resource-dependency hook (useResourceDependencies) now includeagentso agents participate in diff/publish calculations.Key technical decisions
readOnlyflag to uniformly disable edits and hide actions in production.Testing
No new npm dependencies. The change includes a manual verification checklist for UI states and backend behavior; production-guard behavior and promotion flow should be verified manually and via existing automated tests where applicable (no new unit/e2e suites added in this PR).