Skip to content

Make manual handoff explicit#10

Merged
djanogly merged 3 commits intomainfrom
pr/improve-ai-handoff
Mar 5, 2026
Merged

Make manual handoff explicit#10
djanogly merged 3 commits intomainfrom
pr/improve-ai-handoff

Conversation

@djanogly
Copy link
Contributor

@djanogly djanogly commented Mar 5, 2026

This pull request introduces improvements to the AI handoff workflow and enhances both backend logic and frontend user experience. The changes ensure that a clear handoff reason is recorded and displayed when a visitor requests to talk to a human, and that the backend properly updates conversation status and notifies agents. Additionally, the test suite is updated to verify the new behavior.

AI Handoff Workflow and Reason Handling

  • When a visitor clicks the "Talk to human" button, the frontend now passes an explicit handoff reason ("Visitor clicked Talk to human button") to the backend when triggering the handoff mutation (MANUAL_HANDOFF_REASON in ConversationView.tsx). [1] [2]
  • The backend mutation handoffToHuman now updates the conversation fields: sets lastMessageAt, increments unreadByAgent, and triggers an internal notification to alert agents of the new message.
  • Tests are added and updated to verify that the handoff reason is passed correctly and that the backend updates and notification logic are present. [1] [2]

Frontend UI Improvements

  • The inbox page now displays a banner with the handoff reason when a conversation is in the "handoff" state, providing clear context to users.
  • If a conversation was handed off before any AI response was stored, a dedicated message is shown explaining the handoff and its reason.

Test Suite Enhancements

  • Test mocks for mutations and actions are refactored for clarity and to support the new handoff logic, improving test reliability and maintainability. [1] [2]

@djanogly djanogly requested a review from Copilot March 5, 2026 10:41
@vercel
Copy link

vercel bot commented Mar 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opencom-landing Ready Ready Preview, Comment Mar 5, 2026 11:19am
opencom-web Ready Ready Preview, Comment Mar 5, 2026 11:19am

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR makes manual AI→human handoffs more explicit by sending and surfacing a clear handoff reason, while also updating backend persistence behavior and extending tests/UI to reflect the new handoff context.

Changes:

  • Widget now passes an explicit reason when the visitor clicks “Talk to human”.
  • Backend handoffToHuman updates conversation metadata (lastMessageAt, unreadByAgent) and schedules a notification.
  • Inbox UI displays handoff context (banner + empty-AI-review handoff explanation), and tests are updated accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/convex/convex/aiAgent.ts Updates handoff mutation to patch conversation metadata and attempt to notify on handoff.
packages/convex/tests/aiAgentHandoffPath.test.ts Adds assertions to ensure the new persistence/notification behavior exists in source.
apps/widget/src/components/ConversationView.tsx Sends explicit handoff reason for the manual “Talk to human” action.
apps/widget/src/components/ConversationView.test.tsx Refactors action/mutation mocks and adds a test asserting the explicit reason is passed.
apps/web/src/app/inbox/page.tsx Shows handoff reason context in the inbox header and AI review panel.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +671 to +680
await ctx.scheduler.runAfter(0, internal.notifications.notifyNewMessage, {
conversationId: args.conversationId,
messageContent: handoffMessage,
senderType: "bot",
messageId,
senderId: "ai-agent",
sentAt: now,
channel: "chat",
});

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

internal.notifications.notifyNewMessage only notifies agents when senderType === "visitor"; when senderType is "bot" it routes notifications to the visitor instead (push/email). As written, the handoff will likely notify the visitor (potentially redundantly) and won’t alert agents about the handoff/new message. Consider routing an agent-facing event explicitly (e.g., internal.notifications.routeEvent with audience: "agent") or extending notifications to support an agent-notify mode for bot/handoff events, rather than calling notifyNewMessage with senderType: "bot".

Suggested change
await ctx.scheduler.runAfter(0, internal.notifications.notifyNewMessage, {
conversationId: args.conversationId,
messageContent: handoffMessage,
senderType: "bot",
messageId,
senderId: "ai-agent",
sentAt: now,
channel: "chat",
});
await ctx.scheduler.runAfter(0, internal.notifications.routeEvent, {
conversationId: args.conversationId,
audience: "agent",
type: "new_message",
messageId,
sentAt: now,
});

Copilot uses AI. Check for mistakes.
Comment on lines 89 to +104
const mockedUseMutation = useMutation as unknown as ReturnType<typeof vi.fn>;
mockedUseMutation.mockReturnValue(vi.fn().mockResolvedValue(undefined));
mockedUseMutation.mockImplementation((mutationRef: unknown) => {
if (mutationRef === "messages.send") {
return sendMessageMutationMock;
}
if (mutationRef === "visitors.identify") {
return identifyVisitorMutationMock;
}
if (mutationRef === "aiAgent.submitFeedback") {
return submitAiFeedbackMutationMock;
}
if (mutationRef === "aiAgent.handoffToHuman") {
return handoffToHumanMutationMock;
}
return vi.fn().mockResolvedValue(undefined);
});
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

This useMutation mockImplementation expects mutationRef === "messages.send" to return sendMessageMutationMock, but the @opencom/convex mock in this test file doesn’t define api.messages.send (only messages.list). That means the component will call useMutation(undefined) and this branch will never match, so sendMessageMutationMock is unused and the test setup diverges from real usage. Add messages.send: "messages.send" to the mocked api object so the mutation ref mapping is exercised correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +1039 to +1042
<span className="truncate">
{selectedConversation.aiWorkflow.handoffReason ??
"Reason not provided by handoff trigger"}
</span>
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The fallback copy "Reason not provided by handoff trigger" is duplicated in multiple UI spots (banner + AI review empty state). Consider centralizing this string (e.g., a constant/helper) so future copy changes stay consistent across the inbox UI.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 662 to 666
status: "open",
updatedAt: now,
lastMessageAt: now,
unreadByAgent: (conversation.unreadByAgent || 0) + 1,
aiWorkflowState: "handoff",
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

lastMessageAt/updatedAt are set from now, but the inserted handoff message uses its own Date.now() value for createdAt. This can create small ordering inconsistencies (conversation sorted by lastMessageAt slightly after the message timestamp). Consider capturing a single timestamp once and using it for both the message createdAt and the conversation patch fields.

Copilot uses AI. Check for mistakes.
expect(source).toContain('senderId: "ai-agent"');
expect(source).not.toContain('senderId: "system"');
expect(source).toContain("lastMessageAt: now");
expect(source).toContain("unreadByAgent: (conversation.unreadByAgent || 0) + 1");
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

This test is asserting an exact source-code substring for the unreadByAgent update expression. That makes the test very sensitive to harmless refactors/formatting changes (e.g., switching to ??, adding whitespace, or extracting a helper) while not actually validating runtime behavior. Consider loosening the assertion (e.g., check for unreadByAgent: and + 1 separately / use a regex) or adding an integration-style assertion that the mutation actually increments unreadByAgent and updates lastMessageAt.

Suggested change
expect(source).toContain("unreadByAgent: (conversation.unreadByAgent || 0) + 1");
expect(source).toMatch(/unreadByAgent:\s*[^+]*\+\s*1/);

Copilot uses AI. Check for mistakes.
@djanogly djanogly merged commit 80a56a8 into main Mar 5, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants