Skip to content

Conversation

@jaikamat
Copy link
Contributor

@jaikamat jaikamat commented Dec 15, 2025

image

This is rad

See this article which is the only place I could find an example of the intents system.

When a user clicks "Add to Cart" inside an embedded component, what happens? MCP UI solves this with an intent-based message system. Components don't directly modify state—they bubble up intents that the agent interprets

This is a powerful paradigm which allows us to cut scope on creating event handlers while also leveraging agents for stateful interactions.

@jaikamat jaikamat requested a review from ByronDWall December 15, 2025 19:22
@changeset-bot
Copy link

changeset-bot bot commented Dec 15, 2025

⚠️ No Changeset found

Latest commit: 26ddfc9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Dec 15, 2025

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

Project Deployment Review Updated (UTC)
nimbus-documentation Error Error Comment Dec 18, 2025 7:42pm
nimbus-storybook Error Error Dec 18, 2025 7:42pm

Copy link
Contributor

@ByronDWall ByronDWall left a comment

Choose a reason for hiding this comment

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

A super interesting pattern. Really similar to my hacky event listener implementation but also with ai.

We should definitely explore this and other client->server->model data flows more

@jaikamat jaikamat force-pushed the CRAFT-explore-intent-handlers branch from 4b85076 to 06dde3a Compare December 17, 2025 21:52
type?: "button" | "submit" | "reset";
ariaLabel?: string;
/** Optional intent to emit when button is pressed */
intent?: Intent;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ByronDWall It is powerful to give the agent complete control over the intent.

One of the issues I've been grappling with lately is how much control we exert over these UI templates when it comes to statefulness, that is, what priority data do we attach to intent handlers (product name, id, sku, etc)? I don't know that we can make this call in all cases, when we as developers won't have full conversation context. So, I chose to experiment by leaning into the inverse of this to give agents more control (continued below)

Comment on lines +313 to +338
buttonLabel: z
.string()
.describe(
"Label text for the action button. Choose based on the intent type and user context."
),
buttonIntent: z
.object({
type: z
.string()
.describe(
"Intent type identifier in underscore_case. Choose based on what makes sense for the user's query context."
),
description: z
.string()
.describe(
"Human-readable description of what the user wants to do when clicking this button. Consider the current conversation context and what logical next step."
),
payload: z
.record(z.any())
.describe(
"Structured data payload for this intent. Include all relevant entity information."
),
})
.describe(
"Intent configuration for the product card's action button. You decide what this button should do based on the user's query context and next logical steps."
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ByronDWall Here, the agent has complete control over the type of the intent, description, and payload. This has a few benefits:

  1. The agent already has user query and conversation context available to it, so it's more informed than we are
  2. The agent can suggest next steps for a flow, which is a powerful way to steer users in novel directions
  3. Reduced client/server coupling to predefined intents, reducing our maintenance burden

This also has a few drawbacks:

  1. Our templating becomes less deterministic
  2. Few guards against hallucinations (but I remain positive here, as I believe the agent should be able to recover with sufficient type, description, and payload content)

As with most things, we can use the best of both worlds. With this strategy, we can offer a dynamic avenue for agent-steered suggestion. For templates that require a high level of determinism, we can create hardcoded intents with stricter data boundaries.

buildButtonElement({
label: "Add to Cart",
variant: "solid",
label: buttonLabel,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Along with the agent steering the intent, the agent also chooses the button text

console.log("🎯 Intent received:", intent);

// Combine description + structured payload for Claude
const message = `${intent.description}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't have to show a message to the user. It's just here for DX


// Intercept form submissions globally
useEffect(() => {
const handleFormSubmit = (event: SubmitEvent) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was competing with the intents, so it was removed

Comment on lines +58 to +86
const formElement = document.querySelector("form");
if (formElement) {
console.log("📝 Button is in a form, capturing form data...");

const formData = new FormData(formElement);
const data: Record<string, unknown> = {};

// Extract all form fields
formData.forEach((value, key) => {
// Handle file inputs specially (can't serialize File objects)
if (value instanceof File) {
data[key] = {
fileName: value.name,
fileSize: value.size,
fileType: value.type,
};
} else {
data[key] = value;
}
});

// Enhance intent with captured form data
const enhancedIntent = {
...intent,
payload: {
...intent.payload,
formData: data,
},
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we have a form, capture the uncontrolled form data, and attach it to the intent. This then gets sent to Claude so he can do with it what he should

Comment on lines +337 to +339
? `IMPORTANT PERSONA: You are an administrator and analyst for internal merchant tooling, NOT a shopper or consumer. You are a business-user tooling power user of the highest order.
ALWAYS provide UI components that relate to administration, modification, analysis, or similar, for requested entities.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This reliably modified the chat to be more MC-like, that is, focused on the administrator experience rather than the shopper.

@ByronDWall Something I'd like to do is experiment with toggling personas off and on in the chat window to see how it affects the experience, would be neat!

Comment on lines +373 to +380
🔥 CRITICAL: Form Data Capture
When creating buttons inside forms:
- Form data is AUTOMATICALLY captured when any button with an intent is clicked inside a form
- You don't need to set type="submit" - type="button" works fine
- The intent will receive a formData object with all field name/value pairs in the payload
- Example: createButton({ label: "Update Product", intent: { type: "update_product", description: "...", payload: { productId: "123" } } })
- When clicked inside a form, Claude receives: { productId: "123", formData: { productName: "New Name", ... } }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not convinced that we need stuff like this if we solve it in the application itself. Might remove this

name: args.name,
placeholder: args.placeholder,
value: args.defaultValue,
defaultValue: args.defaultValue,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to use uncontrolled values.

.optional()
.describe(
"Button type for HTML forms (default: 'button'). Use 'submit' for form submission buttons."
"Button type for HTML forms (default: 'button'). Note: Form data capture is automatic - any button with an intent inside a form will automatically capture form field values, regardless of type."
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm also not convinced we need application implementation details described at the schema level, but it's hard to reliably test how this affects the chat because it's so nondeterministic

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.

3 participants