diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx index ff73688ae..12b0c78ad 100644 --- a/references/ai-chat/src/components/chat.tsx +++ b/references/ai-chat/src/components/chat.tsx @@ -937,6 +937,24 @@ export function Chat({ Stop )} + {/* Undo — server-side `chat.history.slice(0, -2)` via the + `undo` action, optimistically reflected in the local + `useChat` state. Drops the last user / assistant exchange. + Only meaningful when there's at least one full exchange + and the chat isn't currently streaming. */} + {status !== "streaming" && messages.length >= 2 && ( + + )} diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts index b8d674828..2d90ca6df 100644 --- a/references/ai-chat/src/trigger/chat.ts +++ b/references/ai-chat/src/trigger/chat.ts @@ -401,6 +401,21 @@ export const aiChat = chat }, // #endregion + // #region actionSchema + onAction — typed actions for state-only mutations + // Actions are not turns: only `hydrateMessages` and `onAction` fire, + // no `run()` invocation, no model call. The `undo` action drops the + // last user/assistant exchange so the next message turn sees a + // truncated history. + actionSchema: z.discriminatedUnion("type", [ + z.object({ type: z.literal("undo") }), + ]), + onAction: async ({ action }) => { + if (action.type === "undo") { + chat.history.slice(0, -2); + } + }, + // #endregion + // #region onTurnComplete — persist + background self-review via chat.inject() onTurnComplete: async ({ chatId,