feat(dashboard, api-service): Filter conversations by agent; add recent UI & CTA#10767
feat(dashboard, api-service): Filter conversations by agent; add recent UI & CTA#10767
Conversation
Expose agentId filtering end-to-end and introduce a compact recent conversations UI with upgrade CTA. - API: add agentId to ConversationFilters and include it in getConversationsList query params. - DAL: apply agentId (_agentId) filter in ConversationRepository queries. - UI: refactor RecentConversationsSection to fetch recent conversations for an agent, render skeleton/error/empty states, conversation items, and format timestamps; gate detailed list behind IS_ENTERPRISE and show ConversationsUpgradeCta for non-enterprise users. - Conversations page: gate main conversations content behind IS_ENTERPRISE and split into EnterpriseConversationsContent. - Add ConversationsUpgradeCta component (compact/default variants) to prompt upgrade/contact-sales and link docs. - Update submodule reference (.source). These changes enable agent-scoped conversation listing and a clear upgrade path for non-enterprise installs.
✅ Deploy Preview for dashboard-v2-novu-staging canceled.
|
|
Hey there and thank you for opening this pull request! 👋 We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted. Your PR title is: Requirements:
Expected format: Details: PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name |
|
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 optional agent-scoped conversation filtering across API and DAL, updates MongoDB indexes, gates conversations UI behind an enterprise flag with an upgrade CTA for non-enterprise, refactors recent-conversations to fetch/render agent-specific items, and updates the Changes
Sequence DiagramsequenceDiagram
participant UI as Dashboard (UI)
participant API as API Layer\n(`apps/dashboard/src/api/conversations.ts`)
participant DAL as Repository\n(`libs/dal/.../conversation.repository.ts`)
participant DB as MongoDB
UI->>UI: Check IS_ENTERPRISE
alt Enterprise
UI->>API: GET /conversations?limit=5&agentId=...
API->>DAL: listConversations({ agentId, limit:5, ... })
DAL->>DB: Query {_environmentId, _agentId, ...}
DB-->>DAL: Documents
DAL-->>API: Conversation list
API-->>UI: Filtered results
UI->>UI: Render recent list with links/status/subscriber
else Non-Enterprise
UI->>UI: Render ConversationsUpgradeCta
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/recent-conversations-section.tsx`:
- Around line 161-184: RecentConversationsSkeleton currently renders 4
placeholder rows causing a layout shift; change it to render
RECENT_CONVERSATIONS_DISPLAY_LIMIT placeholders instead: import or reference the
RECENT_CONVERSATIONS_DISPLAY_LIMIT constant and use it as the length in
Array.from inside RecentConversationsSkeleton (keep the existing key pattern
like `skeleton-${index}`), so the skeleton count matches the real display limit
of 5.
- Around line 104-118: RecentConversationItem currently builds a detailPath and
renders a Link even when environmentSlug is missing, resulting in a dead "#"
link; update RecentConversationItem to avoid a non-functional interactive
element by either early-returning (do not render the item) when environmentSlug
is undefined or render a non-interactive container (e.g., a div/buttonless
element with the same classes) instead of Link when environmentSlug is falsy;
change usage of detailPath and the Link import accordingly so the clickable Link
is only used when environmentSlug is present and the non-interactive fallback
preserves visual styles but removes hover/focus interactivity.
In `@apps/dashboard/src/components/conversations/conversations-upgrade-cta.tsx`:
- Around line 86-90: The current code nests a LinkButton inside react-router's
<Link> for an absolute URL, creating nested interactive elements; change this to
either render a plain anchor for the external URL or make LinkButton render the
anchor directly via its asChild prop: replace the outer <Link
to="https://docs.novu.co/agents/overview" ...> wrapper with a standard <a
href="https://docs.novu.co/agents/overview" target="_blank" rel="noreferrer
noopener"> or keep LinkButton but pass asChild so LinkButton renders the <a>
itself (keeping the RiBookMarkedLine icon and size="sm" props) to avoid invalid
nested <a>/<button> elements and fix accessibility.
In `@libs/dal/src/repositories/conversation/conversation.repository.ts`:
- Around line 249-251: The Conversation repository adds an _agentId filter in
the query (see the if (agentId) { query._agentId = agentId; } block) but the
Conversation schema lacks a compound index that covers queries scoped by
_environmentId plus _agentId; add a compound index on the Conversation model
like { _environmentId: 1, _agentId: 1, _id: 1 } to avoid collection scans for
listConversations and, if listConversations is commonly sorted by other fields
such as createdAt or lastActivityAt, also add indexes like { _environmentId: 1,
_agentId: 1, createdAt: 1 } or { _environmentId: 1, _agentId: 1, lastActivityAt:
1 } in the Conversation schema definition so queries using the query._agentId
predicate and various sortBy values are covered.
🪄 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: c25f5e2f-cb74-4fd8-8f9b-4cb24a43c072
📒 Files selected for processing (6)
.sourceapps/dashboard/src/api/conversations.tsapps/dashboard/src/components/agents/recent-conversations-section.tsxapps/dashboard/src/components/conversations/conversations-content.tsxapps/dashboard/src/components/conversations/conversations-upgrade-cta.tsxlibs/dal/src/repositories/conversation/conversation.repository.ts
Refactor RecentConversationItem to extract base and interactive classNames, render a non-interactive div when environmentSlug is missing, and only wrap with a Link when environmentSlug exists. Use RECENT_CONVERSATIONS_DISPLAY_LIMIT for skeleton item count. Update ConversationsUpgradeCta to use LinkButton asChild with a native anchor for the external docs link and remove the unused Link import. Add agent-scoped indexes to the conversation schema (_environmentId + _agentId with _id, createdAt, lastActivityAt) to improve query performance for agent-scoped operations.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/dashboard/src/components/agents/recent-conversations-section.tsx (2)
22-26: Minor: duplicateduseEnvironment()call.
currentEnvironmentis read here and again inRecentConversationsContent(line 57). Since the parent already computesconversationsPath, consider passingcurrentEnvironment?.slug(orconversationsPath) down as a prop to avoid re-subscribing to the context in the child.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/agents/recent-conversations-section.tsx` around lines 22 - 26, The component calls useEnvironment() twice (once here to build conversationsPath and again inside RecentConversationsContent), causing a duplicate context subscription; pass the computed value down instead: compute currentEnvironment?.slug (or conversationsPath) in this component (where useEnvironment() is already used), then add a prop to RecentConversationsContent (e.g., environmentSlug or conversationsPath) and update RecentConversationsContent to consume that prop instead of calling useEnvironment() itself (update usages around buildRoute/ROUTES.ACTIVITY_CONVERSATIONS accordingly).
207-211:conversation._agentIdfallback will render as a raw ObjectId.If neither
agent.agent?.namenoragent?.idis present, the UI displays the raw ObjectId from_agentIdwhich is not user-friendly. Since the parentRecentConversationsSectionalready has the fullAgentResponsein scope, consider threading the agent's display name down toRecentConversationItemand using it as the fallback (all listed conversations belong to that agent anyway).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/agents/recent-conversations-section.tsx` around lines 207 - 211, getAgentName currently falls back to rendering conversation._agentId (a raw ObjectId); instead, pass the parent AgentResponse's display name down from RecentConversationsSection into each RecentConversationItem and use that as the final fallback. Update the caller in RecentConversationsSection to provide a prop (e.g., agentDisplayName or agent.name) to RecentConversationItem, then change getAgentName(conversation: ConversationDto) to accept an optional agentDisplayName parameter and return agent?.agent?.name ?? agent?.id ?? agentDisplayName ?? 'agent'; ensure function and prop names (getAgentName, RecentConversationItem, RecentConversationsSection, AgentResponse, conversation._agentId) are used consistently.
🤖 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/recent-conversations-section.tsx`:
- Around line 59-62: The useFetchConversations call is passing the wrong agent
identifier; update the filters argument in the useFetchConversations invocation
(the call that uses RECENT_CONVERSATIONS_DISPLAY_LIMIT) to use agent._id instead
of agent.identifier so the agentId filter matches the ObjectId stored in the
conversation schema.
---
Nitpick comments:
In `@apps/dashboard/src/components/agents/recent-conversations-section.tsx`:
- Around line 22-26: The component calls useEnvironment() twice (once here to
build conversationsPath and again inside RecentConversationsContent), causing a
duplicate context subscription; pass the computed value down instead: compute
currentEnvironment?.slug (or conversationsPath) in this component (where
useEnvironment() is already used), then add a prop to RecentConversationsContent
(e.g., environmentSlug or conversationsPath) and update
RecentConversationsContent to consume that prop instead of calling
useEnvironment() itself (update usages around
buildRoute/ROUTES.ACTIVITY_CONVERSATIONS accordingly).
- Around line 207-211: getAgentName currently falls back to rendering
conversation._agentId (a raw ObjectId); instead, pass the parent AgentResponse's
display name down from RecentConversationsSection into each
RecentConversationItem and use that as the final fallback. Update the caller in
RecentConversationsSection to provide a prop (e.g., agentDisplayName or
agent.name) to RecentConversationItem, then change getAgentName(conversation:
ConversationDto) to accept an optional agentDisplayName parameter and return
agent?.agent?.name ?? agent?.id ?? agentDisplayName ?? 'agent'; ensure function
and prop names (getAgentName, RecentConversationItem,
RecentConversationsSection, AgentResponse, conversation._agentId) are used
consistently.
🪄 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: 218e3b28-c631-4893-ba5d-773522edf890
📒 Files selected for processing (3)
apps/dashboard/src/components/agents/recent-conversations-section.tsxapps/dashboard/src/components/conversations/conversations-upgrade-cta.tsxlibs/dal/src/repositories/conversation/conversation.schema.ts
| const { conversations, isLoading, isError } = useFetchConversations({ | ||
| limit: RECENT_CONVERSATIONS_DISPLAY_LIMIT, | ||
| filters: { agentId: agent.identifier }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect AgentResponse shape and how _agentId is populated on conversations.
fd -t f 'agents.ts' apps/dashboard/src/api | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
echo "---- agent entity / schema ----"
fd -t f 'agent.entity.ts|agent.schema.ts' libs/dal/src | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
echo "---- conversation creation / _agentId assignment ----"
rg -nP --type=ts -C3 '_agentId\s*[:=]'Repository: novuhq/novu
Length of output: 30067
🏁 Script executed:
fd -t f 'conversations.ts' apps/dashboard/src -type fRepository: novuhq/novu
Length of output: 225
🏁 Script executed:
rg -n 'useFetchConversations' apps/dashboard/src --type=ts --type=tsx -A 5Repository: novuhq/novu
Length of output: 82
🏁 Script executed:
rg -n 'const.*fetchConversations\|export.*useFetchConversations' apps/dashboard/src --type=ts --type=tsx -B 2 -A 10Repository: novuhq/novu
Length of output: 82
🏁 Script executed:
fd -type f 'conversations' apps/dashboard/srcRepository: novuhq/novu
Length of output: 225
🏁 Script executed:
rg 'useFetchConversations' apps/dashboard/src -g '*.ts' -g '*.tsx' -A 5Repository: novuhq/novu
Length of output: 3160
🏁 Script executed:
rg 'fetchConversations|useFetchConversations' apps/dashboard/src -g '*.ts' -g '*.tsx' -B 2 -A 10Repository: novuhq/novu
Length of output: 9446
🏁 Script executed:
cat apps/dashboard/src/api/conversations.tsRepository: novuhq/novu
Length of output: 4696
Use agent._id instead of agent.identifier for the agentId filter.
The conversation schema stores agent references as _agentId: Schema.Types.ObjectId, but you're filtering with agent.identifier (a string slug). The API will send ?agentId=<identifier-slug> to the backend, which tries to match it against an ObjectId field. This causes Mongoose to either throw a cast error or silently return zero results, leaving the Recent Conversations list empty.
Change line 61 to:
filters: { agentId: agent._id },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/agents/recent-conversations-section.tsx` around
lines 59 - 62, The useFetchConversations call is passing the wrong agent
identifier; update the filters argument in the useFetchConversations invocation
(the call that uses RECENT_CONVERSATIONS_DISPLAY_LIMIT) to use agent._id instead
of agent.identifier so the agentId filter matches the ObjectId stored in the
conversation schema.
There was a problem hiding this comment.
But the backend converts the identifier to _id
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for backend conversion of agentId/identifier to _id in conversation-related code
rg -n 'identifier|agentId' apps/api/src --type=ts -g '*conversation*' -A 5 -B 2
echo "---- Check use-cases or services that handle agentId ----"
rg -rn 'agentId' apps/api/src --type=ts -A 5 -B 2
Length of output: 95710
🏁 Script executed:
#!/bin/bash
# Find the list-conversations usecase to see how agentId is processed
rg -rn 'agentId' apps/api/src --type=ts -g '*conversation*' -A 5 -B 2
echo "---- list conversations usecase ----"
fd -t f 'list-conversations' apps/api/src | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
echo "---- conversations controller ----"
fd -t f 'conversations.controller.ts' apps/api/src | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
Length of output: 1897
🏁 Script executed:
#!/bin/bash
# Broader search for conversations listing and agentId handling
rg -rn 'agentId' apps/api/src --type=ts -A 5 -B 2
echo "==== conversations controller candidates ===="
fd -t f 'conversation' apps/api/src --extension ts | grep -v 'agent-conversation'
echo "==== list conversations ===="
rg -rn 'listConversations\|list_conversations\|getConversationsList\|ConversationFilters' apps/api/src --type=ts -A 5 -B 2
Length of output: 92861
🏁 Script executed:
#!/bin/bash
# Find the conversations listing API endpoint in apps/api
fd -t f --extension ts 'conversation' apps/api/src | grep -v 'agent-conversation\|e2e\|spec'
echo "==== search for agentId in non-agents context ===="
rg -rn 'agentId' apps/api/src --type=ts --iglob '!*agent*' -A 5 -B 3
echo "==== dal listConversations call sites ===="
rg -rn 'listConversations' --type=ts -A 5 -B 3
Length of output: 789
🏁 Script executed:
#!/bin/bash
cat libs/dal/src/repositories/conversation/conversation.repository.ts
echo "==== Find conversations API controller/usecase ===="
fd -t f --extension ts . apps/api/src | xargs grep -l 'agentId\|agent_id\|_agentId' 2>/dev/null | grep -v 'agent-conversation\|bridge-executor\|chat-sdk\|agent-inbound\|agent-config'
echo "==== conversations usecase files ===="
fd -t f --extension ts . apps/api/src/app | xargs grep -l 'ConversationFilters\|conversations.*usecase\|usecase.*conversation' 2>/dev/null
Length of output: 8864
🏁 Script executed:
#!/bin/bash
# Find conversations API endpoint/controller outside agents module
rg -rn 'getConversationsList\|conversations.*list\|list.*conversations' apps/api/src --type=ts -B 2 -A 5
echo "==== search for conversation routes ===="
rg -rn "'/conversations'\|\"conversations\"" apps/api/src --type=ts -B 2 -A 3
echo "==== Check what URL dashboard calls for conversations ===="
cat apps/dashboard/src/api/conversations.ts
Length of output: 4741
🏁 Script executed:
#!/bin/bash
# Find the /conversations endpoint handler
rg -rn "conversations" apps/api/src --type=ts -g '*controller*' -B 2 -A 5
echo "==== search for conversation module ===="
fd -t d 'conversation' apps/api/src
echo "==== check all controllers for conversations route ===="
fd -t f 'controller' apps/api/src --extension ts | xargs grep -l 'conversation' 2>/dev/null
Length of output: 194
🏁 Script executed:
#!/bin/bash
# Search across all apps for the conversations GET endpoint
rg -rn "Get.*conversations\|conversations.*Get\|@Get.*conver\|route.*conversation\|conversation.*route" --type=ts -B 2 -A 5
echo "==== find conversations endpoint in all apps ===="
rg -rn "'/conversations'\|\"\/conversations\"\|'/v1/conversations'\|v1.*conversations" --type=ts -B 2 -A 5
Length of output: 210
🏁 Script executed:
#!/bin/bash
# Search for conversations endpoint across all apps
rg -rn "conversations" --type=ts -g '*controller*' -B 2 -A 5
echo "==== find route /conversations ===="
rg -rn "conversations" --type=ts -g '*router*' -B 2 -A 5
echo "==== search all TS files for conversations endpoint ===="
rg -rn "@Get\('conversations'\)" --type=ts -B 2 -A 5
echo "==== look for usecase that handles list conversations with agentId conversion ===="
rg -rn "agentId" --type=ts -g '!**/agents/**' -B 3 -A 5
Length of output: 75597
@scopsy Thanks for the clarification — you’re right that several agent endpoints take an identifier and resolve it to _id server‑side. For conversations, though, the listing path expects the agent’s ObjectId under query param n, and the DAL filters on _n (ObjectId).
Concretely:
- Conversation schema uses _n: ObjectId
- ConversationRepository.listConversations takes n and sets query._n = n
- Dashboard API client appends n when provided in filters and ConversationDto exposes _n
Given that, this component should pass the agent’s _id under n rather than the textual identifier under agentId. Suggested change in apps/dashboard/src/components/agents/recent-conversations-section.tsx:
const { conversations, isLoading, isError } = useFetchConversations({
limit: RECENT_CONVERSATIONS_DISPLAY_LIMIT,
- filters: { agentId: agent.identifier },
+ filters: { n: agent._id },
});This aligns the UI with the conversations API contract and will reliably scope results to the agent. If you prefer keeping a friendlier prop name (agentId) in the hook, we can also translate agentId → n inside useFetchConversations, but today the API client already speaks in terms of n, so wiring it directly here is the smallest change.
🧠 Learnings used
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`
Expose agentId filtering end-to-end and introduce a compact recent conversations UI with upgrade CTA.
These changes enable agent-scoped conversation listing and a clear upgrade path for non-enterprise installs.
What changed? Why was the change needed?
What changed
Agent-scoped conversation listing was implemented end-to-end: API and DAL accept an optional agentId filter, the dashboard UI fetches and displays recent conversations scoped to an agent, and a compact Recent Conversations panel plus an upgrade CTA were added to handle non-enterprise/self-hosted installs. The change enables agent-specific conversation views and provides a clear upgrade/contact-sales path where full conversation listing is enterprise-only.
Affected areas
Key technical decisions
Testing
No automated tests were added; manual verification is recommended for agent-scoped listing correctness, UI loading/error/empty states, enterprise gating behavior, CTA navigation for self-hosted vs SaaS, and telemetry firing on CTA clicks.
Screenshots
Expand for optional sections
Related enterprise PR
Special notes for your reviewer