better-cmdk is an open-source command palette with built-in AI chat. The useAssistant() return value is directly compatible with its chat prop.Install better-cmdk:
bun add better-cmdk
Add it to your Tailwind CSS sources:
@source "node_modules/better-cmdk";
Create the assistant component:
components/assistant.tsx
"use client" // Next.js onlyimport { useState } from "react"import { useAssistant } from "modifywithai"import { CommandMenu } from "better-cmdk"export function Assistant() { const [open, setOpen] = useState(false) const assistant = useAssistant({ actions: [ { name: "createItem", description: "Create a new item in the list", inputSchema: { title: { type: "string", description: "The item title", required: true, }, }, execute: ({ title }) => { console.log("Create item:", title) }, }, { name: "deleteItem", description: "Delete an item from the list", inputSchema: { id: { type: "string", description: "The item ID to delete", required: true, }, }, approvalRequired: true, execute: ({ id }) => { console.log("Delete item:", id) }, }, ], getContext: () => ({ currentPage: "home", items: [], }), }) return <CommandMenu open={open} onOpenChange={setOpen} chat={assistant} />}
Actions defined in actions appear as searchable commands in the palette and as AI-executable tools in chat mode, with built-in approval workflows. See the better-cmdk docs for CSS variables, theming, and component reference.
For full control over the chat interface, build your own component using the useAssistant hook directly:
components/assistant.tsx
"use client" // Next.js onlyimport { useAssistant } from "modifywithai"export function Assistant() { const assistant = useAssistant({ actions: [ { name: "createItem", description: "Create a new item in the list", inputSchema: { title: { type: "string", description: "The item title", required: true, }, }, execute: ({ title }) => { console.log("Create item:", title) }, }, { name: "deleteItem", description: "Delete an item from the list", inputSchema: { id: { type: "string", description: "The item ID to delete", required: true, }, }, approvalRequired: true, // User must confirm execute: ({ id }) => { console.log("Delete item:", id) }, }, ], getContext: () => ({ // Pass current app state so the agent knows what's on screen currentPage: "home", items: [], // Your actual items here }), onAction: (action) => { // Fallback if an action is invoked without a matching execute handler console.log("Unhandled action:", action.name, action.options) }, }) if (!assistant.isReady) { return <div>Loading assistant...</div> } return ( <div> {/* Display messages */} <div> {assistant.messages.map((message) => ( <div key={message.id}> <strong>{message.role}:</strong> {message.parts.map((part, i) => part.type === "text" ? <span key={i}>{part.text}</span> : null )} </div> ))} </div> {/* Input form */} <form onSubmit={assistant.handleSubmit}> <input value={assistant.input} onChange={(e) => assistant.setInput(e.target.value)} placeholder="Try: Create an item called Test" /> <button type="submit" disabled={assistant.status === "streaming"}> Send </button> </form> </div> )}