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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
run: npm install

- name: Install Playwright
run: npx playwright install chromium
run: npx playwright install chromium --no-shell --with-deps

- name: Run Playwright tests
run: npm run test -- --filter @liveblocks/next-sandbox
Expand Down Expand Up @@ -153,7 +153,7 @@ jobs:
run: npm install

- name: Install Playwright
run: npx playwright install chromium
run: npx playwright install chromium --no-shell --with-deps

- name: Run AI Playwright tests
run: npm run test -- --filter @liveblocks/next-ai-kitchen-sink
Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
## vNEXT (not yet published)

## v3.4.0

### `@liveblocks/react`

Tool calls will now stream in while under construction. This means that tools
will render sooner and more often re-render, while `partialArgs` are streaming
in.

> New behavior (>=3.4):
>
> - 1st render: `{ stage: "receiving", partialArgs: {} }`
> - 2nd render: `{ stage: "receiving", partialArgs: { cities: [] } }`
> - 3rd render: `{ stage: "receiving", partialArgs: { cities: [""] } }`
> - 4th render: `{ stage: "receiving", partialArgs: { cities: ["Pa"] } }`
> - 5th render: `{ stage: "receiving", partialArgs: { cities: ["Paris"] } }`
> - etc.
> - Then `{ stage: "executing", args: { cities: "Paris" } }` (same as before)
> - And `{ stage: "executed", args, result }` (same as before)
>
> Before (<3.4):
>
> - Stage "receiving" would never happen
> - 1st render would be with
> `{ stage: "executing", args: { cities: ["Paris"] } }`
> - 2nd render would be with `{ stage: "executed", args, result }`

#### Other changes

- In `RoomProvider`, `initialPresence` and `initialStorage` now get re-evaluated
whenever the room ID (the `id` prop) changes.

### `@liveblocks/react-ui`

- Add a minimal appearance to `AiTool` via a new `variant` prop.
- Improve Markdown rendering during streaming in `AiChat`: incomplete content is
now handled gracefully so things like bold, links, or tables all render
instantly without seeing partial Markdown syntax first.
- Render all messages in `AiChat` as Markdown, including ones from the user.
- Fix Markdown rendering of HTML tags in `AiChat`. (e.g. "Use the `<AiChat />`
component" would render as "Use the `` component")
- Improve shimmer animation visible on elements like the
"Thinking…"/"Reasoning…" placeholders in `AiChat`.

## v3.3.4

### `@liveblocks/client`
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG_PUBLIC.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,59 @@ list and feel free to give them credit at the end of a line, e.g.:

-->

# Week 34 (2025-08-22)

## v3.4.0

### `@liveblocks/react`

Tool calls will now stream in while under construction. This means that tools
will render sooner and more often re-render, while `partialArgs` are streaming
in.

> New behavior (>=3.4):
>
> - 1st render: `{ stage: "receiving", partialArgs: {} }`
> - 2nd render: `{ stage: "receiving", partialArgs: { cities: [] } }`
> - 3rd render: `{ stage: "receiving", partialArgs: { cities: [""] } }`
> - 4th render: `{ stage: "receiving", partialArgs: { cities: ["Pa"] } }`
> - 5th render: `{ stage: "receiving", partialArgs: { cities: ["Paris"] } }`
> - etc.
> - Then `{ stage: "executing", args: { cities: "Paris" } }` (same as before)
> - And `{ stage: "executed", args, result }` (same as before)
>
> Before (<3.4):
>
> - Stage "receiving" would never happen
> - 1st render would be with
> `{ stage: "executing", args: { cities: ["Paris"] } }`
> - 2nd render would be with `{ stage: "executed", args, result }`

#### Other changes

- In `RoomProvider`, `initialPresence` and `initialStorage` now get re-evaluated
whenever the room ID (the `id` prop) changes.

### `@liveblocks/react-ui`

- Add a minimal appearance to `AiTool` via a new `variant` prop.
- Improve Markdown rendering during streaming in `AiChat`: incomplete content is
now handled gracefully so things like bold, links, or tables all render
instantly without seeing partial Markdown syntax first.
- Render all messages in `AiChat` as Markdown, including ones from the user.
- Fix Markdown rendering of HTML tags in `AiChat`. (e.g. "Use the `<AiChat />`
component" would render as "Use the `` component")
- Improve shimmer animation visible on elements like the
"Thinking…"/"Reasoning…" placeholders in `AiChat`.

## Infrastructure

- Improved LiveList conflict resolution that will keep the conflicting element closer to its intended destination.

## Contributors

nvie, marcbouchenoire

# Week 33 (2025-08-15)

## v3.3.4
Expand Down
127 changes: 124 additions & 3 deletions docs/pages/api-reference/liveblocks-client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -923,9 +923,10 @@ passed directly to [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat).
Rendering a tool call can be done before the tool call is executed, which allows
you to display a UI during its entire lifecycle. The tool call stages are:

- `receiving` The tool call is being received. This stage is reserved for future
use, to enable showing a UI while the tool call `args` object is being
streamed in, but before the tool call can actually be made.
- `receiving` (since [3.4]()) The tool call is being received and its args are
being streamed in. During this stage, you can access `partialArgs` to display
a UI while the tool call arguments are still being constructed, but before the
tool call is executed.
- `executing` The tool call is currently executing, or is ready to be. In this
stage, the `args` are fully known, but the result of the tool call is not
known yet.
Expand Down Expand Up @@ -1040,6 +1041,125 @@ value of this shape:
- `{ cancel: true | string }` If the tool call should be cancelled. You can
optionally provide a cancel reason as an instruction to the AI copilot.

#### Handling different tool call stages [#handling-stages]

You can handle all three stages of a tool call in your render function to
provide a smooth user experience during tool call streaming and execution:

```tsx
const bookFlightTool = defineAiTool()({
description: "Book a flight for a user",
parameters: {
type: "object",
properties: {
origin: { type: "string", description: "Departure city" },
destination: { type: "string", description: "Arrival city" },
departureDate: {
type: "string",
description: "Departure date (YYYY-MM-DD)",
},
passengers: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name", "age"],
additionalProperties: false,
},
description: "List of passengers",
},
},
required: ["origin", "destination", "departureDate", "passengers"],
additionalProperties: false,
},
execute: async ({ origin, destination, departureDate, passengers }) => {
const booking = await bookFlight({
origin,
destination,
departureDate,
passengers,
});
return { data: { bookingId: booking.id } };
},
render: ({ stage, partialArgs, args, result }) => {
return (
<AiTool title="Flight Booking" icon="✈️">
{stage === "receiving" && (
<div>
<h4>Preparing flight booking...</h4>
{partialArgs.origin && <p>From: {partialArgs.origin}</p>}
{partialArgs.destination && <p>To: {partialArgs.destination}</p>}
{partialArgs.departureDate && (
<p>Date: {partialArgs.departureDate}</p>
)}
{partialArgs.passengers && (
<div>
<p>Passengers ({partialArgs.passengers.length}):</p>
<ul>
{partialArgs.passengers.map((passenger, index) => (
<li key={index}>
{passenger?.name || "Loading..."}
{passenger?.age && ` (${passenger.age})`}
</li>
))}
</ul>
</div>
)}
</div>
)}
{stage === "executing" && (
<div>
<h4>Booking flight...</h4>
<p>
{args.origin} → {args.destination} on {args.departureDate}
</p>
<p>{args.passengers.length} passenger(s)</p>
</div>
)}
{stage === "executed" && result.data && (
<div>
<h4>Flight booked successfully!</h4>
<p>Booking ID: {result.data.bookingId}</p>
</div>
)}
</AiTool>
);
},
});
```

In this example, the tool arguments stream in progressively during the
`receiving` stage, causing multiple re-renders as each field appears:

- **1st render**: `{ stage: "receiving", partialArgs: {} }`
- **2nd render**: `{ stage: "receiving", partialArgs: { origin: "New York" } }`
- **3rd render**:
`{ stage: "receiving", partialArgs: { origin: "New York", destination: "London" } }`
- **4th render**:
`{ stage: "receiving", partialArgs: { origin: "New York", destination: "London", departureDate: "2024-12-15" } }`
- **5th render**: `{ stage: "receiving", partialArgs: { ..., passengers: [] } }`
- **6th render**:
`{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John" }] } }`
- **7th render**:
`{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John", age: 3 }] } }`
- **8th render**:
`{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John", age: 30 }] } }`
- **Final render**: `{ stage: "executing", args: { /* complete object */ } }`

This demonstrates how each field and nested property appears incrementally,
providing real-time feedback to users as the AI constructs the tool call
arguments.

Arguments are streamed in forward-only order. Once a field begins appearing, all
previous fields are complete and won’t be modified. You’ll never see
`{ origin: "New York", destination: "London" }` followed by
`{ origin: "San Francisco", destination: "London" }`, but you might see
`{ origin: "New" }` then `{ origin: "New York" }` then
`{ origin: "New York", destination: "London" }`.

## Room

Room returned by [`client.enterRoom`][] (or [`client.getRoom`][]).
Expand Down Expand Up @@ -5653,3 +5773,4 @@ the current user receives in the current room.
[`ws`]: https://www.npmjs.com/package/ws
[connection status example]:
https://liveblocks.io/examples/connection-status/nextjs
[3.4]: https://github.com/liveblocks/liveblocks/releases/tag/v3.4.0
4 changes: 4 additions & 0 deletions docs/pages/api-reference/liveblocks-react-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ render: () => (
Content to display inside the tool container. Typically used for
tool-specific UI or descriptions.
</PropertiesListItem>
<PropertiesListItem name="variant" type="'block' | 'minimal'">
The visual appearance of the tool. The `"block"` variant (default) displays
the tool as a block with a border.
</PropertiesListItem>
<PropertiesListItem name="collapsed" type="boolean">
Whether the tool content should be collapsed. When collapsed, only the title
and icon are visible.
Expand Down
48 changes: 48 additions & 0 deletions e2e/next-ai-kitchen-sink/app/cleanup/[chatId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { use, useEffect, useState } from "react";
import { LiveblocksProvider, useDeleteAiChat } from "@liveblocks/react";

function ChatDeleter({ chatId }: { chatId: string }) {
const deleteChat = useDeleteAiChat();
const [deleted, setDeleted] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
try {
deleteChat(chatId);
setDeleted(true);
} catch (err) {
console.error("Failed to delete chat:", err);
setError(err instanceof Error ? err.message : String(err));
}
}, [deleteChat, chatId]);

if (error) {
return <div style={{ color: 'red' }}>Error deleting chat: {error}</div>;
}

if (deleted) {
return <div>Chat deleted</div>;
}

return <div>Deleting chat...</div>;
}

export default function CleanupPage({
params,
}: {
params: Promise<{ chatId: string }>;
}) {
const { chatId } = use(params);

return (
<LiveblocksProvider
authEndpoint="/api/auth/liveblocks"
// @ts-expect-error
baseUrl={process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL}
>
<ChatDeleter chatId={chatId} />
</LiveblocksProvider>
);
}
Loading
Loading