Skip to content

feat(dashboard): agent production environment UI and publish flow fixes NV-7429#10930

Merged
ChmaraX merged 20 commits intonextfrom
feat/agents-environment-promotion-dashboard
Apr 30, 2026
Merged

feat(dashboard): agent production environment UI and publish flow fixes NV-7429#10930
ChmaraX merged 20 commits intonextfrom
feat/agents-environment-promotion-dashboard

Conversation

@ChmaraX
Copy link
Copy Markdown
Contributor

@ChmaraX ChmaraX commented Apr 30, 2026

Summary

  • Hooks agents into the existing publish-to-production modal with warning icons for first-time (inactive) agents and an InlineToast summary
  • Rewrites the publish success modal to a new Figma design with a conditional "Needs setup in production" section linking to agent detail pages
  • Adds a production empty state for the agents list page with illustration
  • Disables "Add Agent" button and table action menus in production environments
  • Adds a first-time agent setup modal shown when opening a newly promoted agent
  • Makes agent info fields (name, description, behavior) read-only in production on both frontend (canEditFields gate) and backend (assertNotProduction guard that returns 403)
  • Moves Bridge URL into its own card with click-to-edit; remains editable in production. Dev environments additionally show a DEVELOPMENT/LOCAL toggle
  • Adds "Viewing in production" InlineToast (soft-warning variant) above the sidebar and integrations tab with a "Switch to dev" CTA
  • Disables Delete menu item and hides "Add provider" button in production
  • Improves setup guide layout spacing to match Figma proportions

Fixes NV-7429

Test plan

  • Verify agents appear in the publish modal with warning icons when publishing for the first time
  • Verify the new publish success modal renders correctly, with "Needs setup in production" section for new agents
  • Verify agents list shows the production empty state (illustration + text) when no agents exist in prod
  • Verify "Add Agent" button is disabled with tooltip in production
  • Verify agent table action menus are disabled in production
  • Verify the agent setup modal appears on first open of a promoted agent in production
  • Verify name/description fields are not editable in production, but the status toggle and bridge URL are
  • Verify backend rejects PATCH requests modifying name/description/behavior in production (403)
  • Verify bridge URL can be edited in production via click-to-edit
  • Verify "Viewing in production" toast appears on Overview and Integrations tabs with working "Switch to dev" link
  • Verify Delete option is disabled in the agent header menu in production
  • Verify "Add provider" is hidden on the integrations tab in production

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

  • api: Introduced a generalized production write guard assertNotProduction(environmentId, organizationId, message) and applied it to agent updates; dev-bridge mutations are blocked in production when devBridgeActive or devBridgeUrl are provided.
  • dashboard: Wired agents into publish modal and publish-success modal; added AgentSetupModal, SetupGuideCard, AgentsProductionEmptyState; made Add/Edit/Delete/table action menus and “Add provider” conditional/disabled in production; moved Bridge URL into a DetailsSidebarCard with inline edit and dev-toggle hidden in production; added “Viewing in production” InlineToast and readOnly UI gating.
  • shared/types/hooks: ResourceToPublish and resource-dependency hook (useResourceDependencies) now include agent so agents participate in diff/publish calculations.
  • feature gating: Publish/publish-success agent UI gated by IS_CONVERSATIONAL_AGENTS_ENABLED.

Key technical decisions

  • Centralized backend production guard now accepts a caller-provided message for clearer 403 responses while enforcing consistent prod-write rejection.
  • Frontend uses an environment-level readOnly flag to uniformly disable edits and hide actions in production.
  • Agents were integrated into existing diff/publish pipelines instead of adding a separate promotion flow to keep promotion UX consistent.

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).

ChmaraX added 10 commits April 30, 2026 11:25
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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7486a365-eae8-4999-8c8c-0065e0ca3f59

📥 Commits

Reviewing files that changed from the base of the PR and between 5d0019e and 8416a1e.

📒 Files selected for processing (1)
  • apps/dashboard/src/components/agents/agent-sidebar-widget.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/dashboard/src/components/agents/agent-sidebar-widget.tsx

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Backend Production Checks
apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts
Refactors production guard to assertNotProduction(environmentId, organizationId, message); computes hasReadOnlyFields (name/description/behavior) and blocks their modification in production; broadens dev-bridge restriction to trigger when devBridgeActive or devBridgeUrl provided.
Publish Types & Hooks
apps/dashboard/src/api/environments.ts, apps/dashboard/src/hooks/use-resource-dependencies.ts
Adds 'agent' to ResourceToPublish.resourceType and returns agents from useResourceDependencies.
Agent Details & Actions
apps/dashboard/src/components/agents/agent-details-header.tsx, apps/dashboard/src/components/agents/agents-table.tsx, apps/dashboard/src/components/agents/agent-integrations-tab.tsx
Reads readOnly from environment; disables/hides delete, row actions, and integration add/remove when read-only; shows CTA toast to switch environment.
Sidebar / Inline Editing
apps/dashboard/src/components/agents/agent-sidebar-widget.tsx, apps/dashboard/src/components/agents/agent-overview-tab.tsx
Gates name/description/bridge editing with readOnly; refactors bridge URL into inline-edit overlay with blur/Enter/Escape persistence; adjusts layout alignment.
Integration Guide & Resolve Flows
apps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-layout.tsx, apps/dashboard/src/components/agents/resolve-agent-integration-guide.tsx
Extracts AgentIntegrationGuideHeader; derives connection from integrationLink.connectedAt (badge shows “Connected”); wraps provider setup components with shared header/status wrapper and adjusts removal control disabling.
Setup UI & Modal
apps/dashboard/src/components/agents/setup-guide-card.tsx, apps/dashboard/src/components/agents/agent-setup-guide.tsx, apps/dashboard/src/components/agents/agent-setup-modal.tsx
Adds SetupGuideCard (expandable container); simplifies AgentSetupGuide to use it; introduces AgentSetupModal (checklist, docs link, setup CTA).
List / Empty State
apps/dashboard/src/components/agents/agents-list.tsx, apps/dashboard/src/components/agents/agents-production-empty-state.tsx
Detects production/read-only env to show AgentsProductionEmptyState when empty; disables “Add Agent” in read-only and surfaces tooltip/CTA to switch environment.
Publishing UX & Success Modal
apps/dashboard/src/components/header-navigation/publish-modal.tsx, apps/dashboard/src/components/header-navigation/publish-success-modal.tsx
Feature-flagged Agents group in Publish modal; warns about inactive publishing; success modal aggregates agent results, lists newly promoted agents needing production setup.
Primitives & Pages
apps/dashboard/src/components/primitives/inline-toast.tsx, apps/dashboard/src/pages/agent-details.tsx
Adds soft-warning InlineToast variant; agent-details queries integrations and conditionally shows AgentSetupModal when read-only, agent inactive, and no connected integrations.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • scopsy
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits format with valid type 'feat', valid scope 'dashboard', lowercase imperative description, and ends with Linear ticket 'NV-7429'.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 10 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

- 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
ChmaraX added 3 commits April 30, 2026 14:21
- 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
@ChmaraX ChmaraX marked this pull request as draft April 30, 2026 12:48
- 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
@ChmaraX ChmaraX changed the title feat(dashboard): wire agent resources into the publish modal feat(dashboard): agent production environment UI and publish flow Apr 30, 2026
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Apr 30, 2026

@ChmaraX ChmaraX marked this pull request as ready for review April 30, 2026 15:37
…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
Base automatically changed from feat/agents-environment-promotion to next April 30, 2026 15:37
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 30, 2026

Deploy preview added

Name Link
🔨 Latest commit 8416a1e
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69f38761a587510008e7dbac
😎 Deploy Preview https://deploy-preview-10930.dashboard-v2.novu-staging.co
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@ChmaraX ChmaraX changed the title feat(dashboard): agent production environment UI and publish flow feat(dashboard): agent production environment UI and publish flow fixes NV-7429 Apr 30, 2026
Comment thread apps/dashboard/src/components/agents/agent-sidebar-widget.tsx Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Reset the setup dismissal per agent/environment.

setupModalDismissed is 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 win

Block status toggling in read-only environments.

agent.active can still be toggled in production because this control uses canWrite instead of canEditFields. 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 win

Flatten 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

📥 Commits

Reviewing files that changed from the base of the PR and between 264d97e and 18de3f8.

📒 Files selected for processing (19)
  • apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts
  • apps/dashboard/src/api/environments.ts
  • apps/dashboard/src/components/agents/agent-details-header.tsx
  • apps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-layout.tsx
  • apps/dashboard/src/components/agents/agent-integration-guides/resolve-agent-integration-guide.tsx
  • apps/dashboard/src/components/agents/agent-integrations-tab.tsx
  • apps/dashboard/src/components/agents/agent-overview-tab.tsx
  • apps/dashboard/src/components/agents/agent-setup-guide.tsx
  • apps/dashboard/src/components/agents/agent-setup-modal.tsx
  • apps/dashboard/src/components/agents/agent-sidebar-widget.tsx
  • apps/dashboard/src/components/agents/agents-list.tsx
  • apps/dashboard/src/components/agents/agents-production-empty-state.tsx
  • apps/dashboard/src/components/agents/agents-table.tsx
  • apps/dashboard/src/components/agents/setup-guide-card.tsx
  • apps/dashboard/src/components/header-navigation/publish-modal.tsx
  • apps/dashboard/src/components/header-navigation/publish-success-modal.tsx
  • apps/dashboard/src/components/primitives/inline-toast.tsx
  • apps/dashboard/src/hooks/use-resource-dependencies.ts
  • apps/dashboard/src/pages/agent-details.tsx

Comment thread apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts Outdated
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">
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment thread apps/dashboard/src/components/agents/agents-list.tsx Outdated
Comment on lines +258 to +289
{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>
)}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/dashboard/src/components/primitives/tooltip.tsx

Repository: novuhq/novu

Length of output: 3113


🏁 Script executed:

rg "TooltipTrigger" apps/dashboard/src --type tsx -A 2 -B 2

Repository: novuhq/novu

Length of output: 83


🏁 Script executed:

rg "TooltipTrigger" apps/dashboard/src --type-add 'web:*.tsx;*.ts' -t web -A 2 -B 1

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

grep -r "TooltipTrigger" apps/dashboard/src --include="*.tsx" -A 2 -B 1

Repository: novuhq/novu

Length of output: 50368


🏁 Script executed:

grep -A 5 "TooltipTrigger asChild" apps/dashboard/src/components/agents/agents-table.tsx | head -20

Repository: novuhq/novu

Length of output: 495


🏁 Script executed:

grep -B 2 -A 8 "TooltipTrigger asChild" apps/dashboard/src/components/primitives/permission-button.tsx | head -30

Repository: 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.

Suggested change
{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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the existing TooltipTrigger pattern used across the dashboard (same approach as CreateLayoutButton for disabled workflow/layout buttons). No change needed here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 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.

Comment on lines +17 to +31
<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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted — will add accessible semantics (aria-expanded, aria-controls) as a follow-up. Keeping this PR focused on the production environment functionality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +104 to +113
<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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted — will add accessible semantics as a follow-up. Keeping this PR focused on the production environment functionality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment thread apps/dashboard/src/pages/agent-details.tsx
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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e76e064 and 5d0019e.

📒 Files selected for processing (4)
  • apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts
  • apps/dashboard/src/components/agents/agent-sidebar-widget.tsx
  • apps/dashboard/src/components/agents/agents-list.tsx
  • apps/dashboard/src/pages/agent-details.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/dashboard/src/pages/agent-details.tsx

Comment thread apps/dashboard/src/components/agents/agent-sidebar-widget.tsx
Comment thread apps/dashboard/src/components/agents/agent-sidebar-widget.tsx
Comment on lines +214 to +215
const isProductionEnv =
Boolean(currentEnvironment) && (readOnly || currentEnvironment?.type !== EnvironmentTypeEnum.DEV);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
@ChmaraX ChmaraX merged commit b1f774d into next Apr 30, 2026
33 checks passed
@ChmaraX ChmaraX deleted the feat/agents-environment-promotion-dashboard branch April 30, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant