Skip to content

feat(api-service): improve managed-agent MCP setup gate UX fixes NV-7906#11350

Merged
ChmaraX merged 1 commit into
nextfrom
nv-7906-managed-agent-mcp-setup-gate-typing-ack-and-setup-complete
May 29, 2026
Merged

feat(api-service): improve managed-agent MCP setup gate UX fixes NV-7906#11350
ChmaraX merged 1 commit into
nextfrom
nv-7906-managed-agent-mcp-setup-gate-typing-ack-and-setup-complete

Conversation

@ChmaraX
Copy link
Copy Markdown
Contributor

@ChmaraX ChmaraX commented May 29, 2026

Summary

  • Show typing (or first-message ack) before the managed-agent MCP setup gate so Slack/Teams/Telegram get immediate feedback while OAuth is pending
  • Start typing before replaying the parked inbound turn after OAuth completes
  • Platform-aware setup-complete card copy: short celebration when typing will run; processing hint on Email/WhatsApp or when ack is off
  • Stale setup card resolve uses celebration-only copy (no replay path)
  • Nudge markdown when user sends another message while still gated

Linear

https://linear.app/novu/issue/NV-7906/managed-agent-mcp-setup-gate-typing-ack-and-setup-complete-copy

Test plan

  • First gated inbound on Slack with acknowledgeOnReceived: typing + setup card, no Anthropic dispatch
  • Follow-up while gated: nudge message + card refresh
  • OAuth complete on Slack: resolved card (You're all set!) + typing + replay of parked message
  • OAuth complete on Email / ack off: card includes automatic replay hint
  • Message after connect with stale pending: card resolves without "working on your message" copy; new message dispatches

Made with Cursor

What changed

The PR improves the managed-agent MCP OAuth setup flow UX by showing typing indicators and acknowledgment before the setup gate (on supported platforms like Slack/Teams/Telegram), providing immediate feedback while OAuth is pending. It adds platform-aware setup card copy that celebrates completion on platforms with typing indicators, or includes automatic replay hints on Email/WhatsApp or when acknowledge-on-receipt is disabled. Follow-up messages while gated now trigger nudge prompts and card refreshes. Stale setup cards are resolved with celebration-only copy.

Affected areas

  • api: Refactored agent inbound handler to show typing/acknowledgment earlier (before the setup gate), added startTypingInConversation helper to ChatSdkService for triggering typing indicators, updated managed-agent setup usecases to conditionally show typing before replay, introduced platform-aware setup card builders with showProcessingHint parameter to control completion copy, and added nudge markdown messages for gated interactions.

Key technical decisions

  • Moved acknowledgeOnReceived behavior earlier in the inbound flow (after managed-runtime detection) so it executes before the setup gate, ensuring immediate platform feedback.
  • Added showProcessingHint parameter to setup card builders to conditionally display processing hints vs. celebration-only copy based on whether typing will be shown.
  • New startTypingInConversation helper in ChatSdkService abstracts typing indicator logic with platform-aware fallbacks and graceful error handling.
  • Distinguished between celebratory ("You're all set!") and processing-hint completion text, selected based on platform membership and typing availability.

Testing

New unit test added to cover managed-agent inbound gating with acknowledgeOnReceived enabled, verifying typing is initiated and setup gate executes before dispatch.

Review Change Stack

Show typing before the setup gate and before OAuth replay, nudge on gated
follow-ups, and use platform-aware setup-complete card copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 29, 2026

NV-7906

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 29, 2026

PR author is not in the allowed authors list.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 29, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit 29d144e
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/6a19575f9bf0770008f9a014

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR adds typing indicator support to the managed-agent setup flow. A new ChatSdkService method enables typing on supported platforms, the inbound handler is adjusted to show typing before the setup gate, setup cards conditionally display processing hints, setup completion triggers typing before replaying held messages, and nudge messaging guides users through the OAuth/MCP setup. New tests validate the behavior.

Changes

Typing indicator integration for managed-agent setup flow

Layer / File(s) Summary
Typing indicator infrastructure in ChatSdkService
apps/api/src/app/agents/services/chat-sdk.service.ts
ChatSdkService adds startTypingInConversation(...) method to start typing on conversation threads for supported platforms. Error handling for evicted cache disposal and chat state logger warn/error branches are reformatted with consistent captureAgentException/captureAgentWarning calls.
Setup card presentation constants and builder updates
apps/api/src/app/agents/usecases/managed-agent-setup/setup-card.helpers.ts, apps/api/src/app/agents/usecases/managed-agent-setup/setup-card.builder.ts
New constants define setup complete messaging variants and SETUP_GATE_NUDGE_MARKDOWN. buildSetupCard and buildSetupCardForMcps now accept optional showProcessingHint parameter to conditionally select completion card text.
Setup completion flow with typing indicator
apps/api/src/app/agents/usecases/managed-agent-setup/complete-managed-agent-setup.usecase.ts
CompleteManagedAgentSetup injects ChatSdkService, computes willShowTypingBeforeReplay from config and platform membership, inverts showProcessingHint on resolved cards, and calls showTypingBeforeSetupReplay(...) helper to trigger typing before replaying held messages.
Setup inbound gate nudge messaging
apps/api/src/app/agents/usecases/managed-agent-setup/handle-managed-agent-setup-inbound.usecase.ts
Imports SETUP_GATE_NUDGE_MARKDOWN and posts nudge message when existing pending setup card is detected, explicitly disables processing hints on resolved stale setup cards.
Inbound handler acknowledgeOnReceived timing adjustment
apps/api/src/app/agents/services/agent-inbound-handler.service.ts
acknowledgeOnReceived handling is moved earlier in the flow—immediately after determining if agent is managed—so typing feedback triggers before managed-agent setup parking logic.
Test coverage for typing and managed-agent inbound gating
apps/api/src/app/agents/services/agent-inbound-handler.service.spec.ts
Test factory exposes additional managed-agent mocks for assertions. New test case validates that acknowledgeOnReceived triggers thread.startTyping before the managed-agent setup gate and prevents dispatch.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • novuhq/novu#11263: Overlaps in Sentry instrumentation updates to apps/api/src/app/agents/services/chat-sdk.service.ts around error capture calls for evicted cached Chat instance disposal.

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) and scope (api-service), includes lowercase imperative description, and ends with Linear ticket reference (NV-7906).
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.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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

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

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: 1

🧹 Nitpick comments (2)
apps/api/src/app/agents/usecases/managed-agent-setup/complete-managed-agent-setup.usecase.ts (1)

273-275: ⚡ Quick win

Deduplicate the typing-gate condition to prevent card/typing drift.

willShowTypingBeforeReplay (Lines 274-275) and the guard inside showTypingBeforeSetupReplay (Line 340) are logical negations of the same predicate. They drive two coupled decisions — showProcessingHint: !willShowTypingBeforeReplay on the resolved card and whether typing actually fires. If one is edited without the other, the card copy can contradict the real behavior (e.g. celebration copy shown but no typing). Extract a single predicate and reuse it in both places.

♻️ Proposed refactor
+  private shouldShowTypingBeforeReplay(conversation: ConversationEntity, config: ResolvedAgentConfig): boolean {
+    const platformThreadId = conversation.channels?.[0]?.platformThreadId;
+
+    return Boolean(
+      config.acknowledgeOnReceived && PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform) && platformThreadId
+    );
+  }
-    const platformThreadId = conversation.channels?.[0]?.platformThreadId;
-    const willShowTypingBeforeReplay =
-      config.acknowledgeOnReceived && PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform) && !!platformThreadId;
+    const willShowTypingBeforeReplay = this.shouldShowTypingBeforeReplay(conversation, config);
   private async showTypingBeforeSetupReplay(
     conversation: ConversationEntity,
     agent: Pick<AgentEntity, '_id'>,
     config: ResolvedAgentConfig
   ): Promise<void> {
     const platformThreadId = conversation.channels?.[0]?.platformThreadId;

-    if (!config.acknowledgeOnReceived || !PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform) || !platformThreadId) {
+    if (!this.shouldShowTypingBeforeReplay(conversation, config) || !platformThreadId) {
       return;
     }

Also applies to: 338-342

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/agents/usecases/managed-agent-setup/complete-managed-agent-setup.usecase.ts`
around lines 273 - 275, The two places use inverted copies of the same predicate
causing potential drift; extract a single boolean predicate (e.g.
shouldShowTypingBeforeReplay) computed once from the conversation/config
(replace current willShowTypingBeforeReplay) and reuse it in both the card
resolution (set showProcessingHint: !shouldShowTypingBeforeReplay) and the
typing guard used by showTypingBeforeSetupReplay so both decisions come from the
same value (update references to willShowTypingBeforeReplay and the guard in
showTypingBeforeSetupReplay to use shouldShowTypingBeforeReplay).
apps/api/src/app/agents/services/agent-inbound-handler.service.spec.ts (1)

298-351: ⚡ Quick win

Assert the typing-before-gate ordering the test name promises.

The test is titled "should show typing before managed-agent setup gate" but only verifies both thread.startTyping and setupInbound were each called once — it would still pass if the order were reversed. Add an explicit ordering assertion to actually pin down the behavior this PR changes.

💚 Suggested ordering assertion
       expect(thread.startTyping.calledOnceWith('Thinking...')).to.equal(true);
       expect(setupInbound.calledOnce).to.equal(true);
+      sinon.assert.callOrder(thread.startTyping, setupInbound);
       expect(managedAgentService.dispatch.called).to.equal(false);

Separately, this case re-builds the entire handler inline rather than using makeHandler, which now already exposes managedAgentService/agentRepository/subscriberRepository. Extending makeHandler with subscriberResolver/subscriberRepository overrides and reusing it here would cut the duplication — optional, given focused-test stub conventions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/agents/services/agent-inbound-handler.service.spec.ts`
around lines 298 - 351, The test title promises startTyping happens before the
managed-agent setup gate but currently only asserts both were called; add an
explicit ordering check (e.g., use sinon.assert.callOrder or equivalent) to
assert thread.startTyping was invoked before
handleManagedAgentSetupInbound.execute (setupInbound) when
AgentInboundHandler.handle runs; optionally refactor to reuse makeHandler by
passing overrides for
subscriberResolver/subscriberRepository/managedAgentService/agentRepository to
remove the inline handler construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/app/agents/services/agent-inbound-handler.service.ts`:
- Around line 391-409: The awaited call to thread.startTyping('Thinking...') can
throw and currently aborts subsequent setup/parking logic; make typing non-fatal
by catching errors instead of allowing the rejection to propagate: replace the
direct awaited call in agent-inbound-handler.service with a resilient form
(e.g., call thread.startTyping('Thinking...').catch(toDeliveryError) or wrap the
await in try/catch), and on error log a warning and call captureAgentWarning
with component 'agent-inbound-handler' and operation 'start-typing' so failures
are recorded but the code continues to the managed-agent parking/setup steps
(keep the existing acknowledge fallback branch behavior for first messages
intact).

---

Nitpick comments:
In `@apps/api/src/app/agents/services/agent-inbound-handler.service.spec.ts`:
- Around line 298-351: The test title promises startTyping happens before the
managed-agent setup gate but currently only asserts both were called; add an
explicit ordering check (e.g., use sinon.assert.callOrder or equivalent) to
assert thread.startTyping was invoked before
handleManagedAgentSetupInbound.execute (setupInbound) when
AgentInboundHandler.handle runs; optionally refactor to reuse makeHandler by
passing overrides for
subscriberResolver/subscriberRepository/managedAgentService/agentRepository to
remove the inline handler construction.

In
`@apps/api/src/app/agents/usecases/managed-agent-setup/complete-managed-agent-setup.usecase.ts`:
- Around line 273-275: The two places use inverted copies of the same predicate
causing potential drift; extract a single boolean predicate (e.g.
shouldShowTypingBeforeReplay) computed once from the conversation/config
(replace current willShowTypingBeforeReplay) and reuse it in both the card
resolution (set showProcessingHint: !shouldShowTypingBeforeReplay) and the
typing guard used by showTypingBeforeSetupReplay so both decisions come from the
same value (update references to willShowTypingBeforeReplay and the guard in
showTypingBeforeSetupReplay to use shouldShowTypingBeforeReplay).
🪄 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: 1439cb23-254d-4a6c-a806-534b3241566e

📥 Commits

Reviewing files that changed from the base of the PR and between 6911f29 and 29d144e.

📒 Files selected for processing (7)
  • apps/api/src/app/agents/services/agent-inbound-handler.service.spec.ts
  • apps/api/src/app/agents/services/agent-inbound-handler.service.ts
  • apps/api/src/app/agents/services/chat-sdk.service.ts
  • apps/api/src/app/agents/usecases/managed-agent-setup/complete-managed-agent-setup.usecase.ts
  • apps/api/src/app/agents/usecases/managed-agent-setup/handle-managed-agent-setup-inbound.usecase.ts
  • apps/api/src/app/agents/usecases/managed-agent-setup/setup-card.builder.ts
  • apps/api/src/app/agents/usecases/managed-agent-setup/setup-card.helpers.ts

Comment on lines +391 to +409
if (config.acknowledgeOnReceived) {
const supportsTyping = PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform);

if (supportsTyping) {
await thread.startTyping('Thinking...');
} else if (isFirstMessage && message.id) {
thread
.createSentMessageFromMessage(message)
.addReaction(ACKNOWLEDGE_FALLBACK_EMOJI)
.catch((err) => {
this.logger.warn(err, `[agent:${agentId}] Failed to add ack reaction to first message`);
captureAgentWarning(err, {
component: 'agent-inbound-handler',
operation: 'add-ack-reaction',
agentId,
});
});
}
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the typing call so a typing failure can't skip the setup gate.

await thread.startTyping('Thinking...') is awaited with no error handling, and it now runs before the managed-agent parking logic (Line 413). If startTyping rejects (delivery/network error), the rejection propagates out of handle, so the message is never parked and the setup card is never shown — the opposite of the UX this PR is trying to deliver. Note the fallback reaction branch already swallows its errors via .catch(...), and ChatSdkService.startTypingInConversation routes failures through .catch(toDeliveryError); the typing branch here should be equally resilient.

🛡️ Proposed fix to make typing non-fatal
       if (supportsTyping) {
-        await thread.startTyping('Thinking...');
+        await thread.startTyping('Thinking...').catch((err) => {
+          this.logger.warn(err, `[agent:${agentId}] Failed to start typing indicator`);
+          captureAgentWarning(err, {
+            component: 'agent-inbound-handler',
+            operation: 'start-typing',
+            agentId,
+          });
+        });
       } else if (isFirstMessage && message.id) {
📝 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
if (config.acknowledgeOnReceived) {
const supportsTyping = PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform);
if (supportsTyping) {
await thread.startTyping('Thinking...');
} else if (isFirstMessage && message.id) {
thread
.createSentMessageFromMessage(message)
.addReaction(ACKNOWLEDGE_FALLBACK_EMOJI)
.catch((err) => {
this.logger.warn(err, `[agent:${agentId}] Failed to add ack reaction to first message`);
captureAgentWarning(err, {
component: 'agent-inbound-handler',
operation: 'add-ack-reaction',
agentId,
});
});
}
}
if (config.acknowledgeOnReceived) {
const supportsTyping = PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform);
if (supportsTyping) {
await thread.startTyping('Thinking...').catch((err) => {
this.logger.warn(err, `[agent:${agentId}] Failed to start typing indicator`);
captureAgentWarning(err, {
component: 'agent-inbound-handler',
operation: 'start-typing',
agentId,
});
});
} else if (isFirstMessage && message.id) {
thread
.createSentMessageFromMessage(message)
.addReaction(ACKNOWLEDGE_FALLBACK_EMOJI)
.catch((err) => {
this.logger.warn(err, `[agent:${agentId}] Failed to add ack reaction to first message`);
captureAgentWarning(err, {
component: 'agent-inbound-handler',
operation: 'add-ack-reaction',
agentId,
});
});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/agents/services/agent-inbound-handler.service.ts` around
lines 391 - 409, The awaited call to thread.startTyping('Thinking...') can throw
and currently aborts subsequent setup/parking logic; make typing non-fatal by
catching errors instead of allowing the rejection to propagate: replace the
direct awaited call in agent-inbound-handler.service with a resilient form
(e.g., call thread.startTyping('Thinking...').catch(toDeliveryError) or wrap the
await in try/catch), and on error log a warning and call captureAgentWarning
with component 'agent-inbound-handler' and operation 'start-typing' so failures
are recorded but the code continues to the managed-agent parking/setup steps
(keep the existing acknowledge fallback branch behavior for first messages
intact).

@ChmaraX ChmaraX merged commit b230fbf into next May 29, 2026
36 checks passed
@ChmaraX ChmaraX deleted the nv-7906-managed-agent-mcp-setup-gate-typing-ack-and-setup-complete branch May 29, 2026 11:47
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