Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .changeset/socks-proxy-support.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: August 2026 - Human in the Loop
description: Mock paused tool calls, approvals, and continuations for the AI SDK with @shadcn/helpers.
date: 2026-08-12
---

**@shadcn/helpers** can now mock human-in-the-loop flows for the AI SDK. A
scripted conversation can pause for real user input, wait for an approval, and
continue with whatever the user decided.

Everything streams through the real
`useChat` lifecycle, so your tool cards, approval prompts, and question flows
behave exactly as they would in production.

```ts showLineNumbers
import { createChat } from "@shadcn/helpers/ai-sdk"

const chat = createChat<ChatMessage>()
.user("Help me plan the next prototype.")
.assistant(({ writer }) => {
writer.text("A couple of questions before I start.")
writer.tool("askQuestions", { input: { questions } })
})
.assistant(({ writer, toolCall }) => {
writer.text(
toolCall?.name === "askQuestions" && toolCall.output
? `Starting with ${toolCall.output.answers.direction}.`
: "Starting now."
)
})
```

Pass `needsApproval` to pause behind the user's decision. The scripted output
streams after approval, and denial streams automatically.

```ts showLineNumbers
chat
.assistant(({ writer }) => {
writer.text("That will archive 3 drafts. I need your approval.")
writer.tool("archiveDrafts", {
input: { count: 3 },
needsApproval: true,
output: { archived: 3 },
})
})
.assistant(({ writer, toolCall }) => {
writer.text(
toolCall?.approved ? "Archived 3 drafts." : "Okay, leaving them in place."
)
})
```

<div className="flex flex-wrap gap-2">
<Button asChild size="sm">
<Link
href="/docs/helpers/ai-sdk#human-in-the-loop"
className="mt-6 no-underline!"
>
Read the Docs
</Link>
</Button>
</div>
12 changes: 5 additions & 7 deletions apps/v4/content/docs/components/base/button-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ The `ButtonGroupSeparator` component visually divides buttons within a group.

Use this component to display text within a button group.

| Prop | Type | Default |
| --------- | --------- | ------- |
| `asChild` | `boolean` | `false` |
| Prop | Type | Default |
| -------- | -------------------- | ------- |
| `render` | `React.ReactElement` | |

```tsx
<ButtonGroup>
Expand All @@ -224,7 +224,7 @@ Use this component to display text within a button group.
</ButtonGroup>
```

Use the `asChild` prop to render a custom component as the text, for example a label.
Use the `render` prop to render a custom component as the text, for example a label.

```tsx showLineNumbers
import { ButtonGroupText } from "@/components/ui/button-group"
Expand All @@ -233,9 +233,7 @@ import { Label } from "@/components/ui/label"
export function ButtonGroupTextDemo() {
return (
<ButtonGroup>
<ButtonGroupText asChild>
<Label htmlFor="name">Text</Label>
</ButtonGroupText>
<ButtonGroupText render={<Label htmlFor="name" />}>Text</ButtonGroupText>
<Input placeholder="Type something here..." id="name" />
</ButtonGroup>
)
Expand Down
16 changes: 7 additions & 9 deletions apps/v4/content/docs/components/base/data-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,11 @@ export const columns = columnHelper.columns([

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
<DropdownMenuTrigger
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
Expand Down Expand Up @@ -703,10 +703,8 @@ export function DataTable<TData extends RowData>({
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns
</Button>
<DropdownMenuTrigger render={<Button variant="outline" className="ml-auto" />}>
Columns
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
Expand Down
7 changes: 3 additions & 4 deletions apps/v4/content/docs/components/base/field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,9 @@ Flex column that groups control and descriptions when the label sits beside the

Label styled for both direct inputs and nested `Field` children.

| Prop | Type | Default |
| ----------- | --------- | ------- |
| `className` | `string` | |
| `asChild` | `boolean` | `false` |
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `className` | `string` | |

```tsx
<FieldLabel htmlFor="email">Email</FieldLabel>
Expand Down
40 changes: 16 additions & 24 deletions apps/v4/content/docs/components/base/sidebar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,11 @@ Use the `SidebarHeader` component to add a sticky header to the sidebar.
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
Select Workspace
<ChevronDown className="ml-auto" />
</SidebarMenuButton>
<DropdownMenuTrigger render={<SidebarMenuButton />}>
Select Workspace
<ChevronDown className="ml-auto" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[--radix-popper-anchor-width]">
<DropdownMenuContent>
<DropdownMenuItem>
<span>Acme Inc</span>
</DropdownMenuItem>
Expand Down Expand Up @@ -344,11 +342,9 @@ To make a `SidebarGroup` collapsible, wrap it in a `Collapsible`.
```tsx showLineNumbers
<Collapsible defaultOpen className="group/collapsible">
<SidebarGroup>
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
Help
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</CollapsibleTrigger>
<SidebarGroupLabel render={<CollapsibleTrigger />}>
Help
<ChevronDown className="ml-auto transition-transform group-data-open/collapsible:rotate-180" />
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent />
Expand Down Expand Up @@ -380,11 +376,9 @@ The `SidebarMenu` component is used for building a menu within a `SidebarGroup`.
<SidebarMenu>
{projects.map((project) => (
<SidebarMenuItem key={project.name}>
<SidebarMenuButton asChild>
<a href={project.url}>
<project.icon />
<span>{project.name}</span>
</a>
<SidebarMenuButton render={<a href={project.url} />}>
<project.icon />
<span>{project.name}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
Expand All @@ -395,13 +389,13 @@ The `SidebarMenu` component is used for building a menu within a `SidebarGroup`.

The `SidebarMenuButton` component is used to render a menu button within a `SidebarMenuItem`.

By default, the `SidebarMenuButton` renders a button but you can use the `asChild` prop to render a different component such as a `Link` or an `a` tag.
By default, the `SidebarMenuButton` renders a button but you can use the `render` prop to render a different component such as a `Link` or an `a` tag.

Use the `isActive` prop to mark a menu item as active.

```tsx showLineNumbers
<SidebarMenuButton asChild isActive>
<a href="#">Home</a>
<SidebarMenuButton render={<a href="#" />} isActive>
Home
</SidebarMenuButton>
```

Expand All @@ -411,11 +405,9 @@ The `SidebarMenuAction` component is used to render a menu action within a `Side

```tsx showLineNumbers
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="#">
<Home />
<span>Home</span>
</a>
<SidebarMenuButton render={<a href="#" />}>
<Home />
<span>Home</span>
</SidebarMenuButton>
<SidebarMenuAction>
<Plus /> <span className="sr-only">Add Project</span>
Expand Down
Loading
Loading