Skip to content
Open
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
32 changes: 31 additions & 1 deletion app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,37 @@ export async function POST(request: Request) {
const isReasoningModel = capabilities?.reasoning === true;
const supportsTools = capabilities?.tools === true;

const modelMessages = await convertToModelMessages(uiMessages);
// Clean up unhandled tool approvals: strip pending parts from model context and mark them as denied in DB
const isPendingApproval = (part: Record<string, unknown>) =>
part.state === "approval-requested";

const cleanedMessages = uiMessages
.map((msg) => {
if (msg.role !== "assistant") return msg;
const cleanedParts = msg.parts.filter(
(part) => !isPendingApproval(part as Record<string, unknown>),
);
if (cleanedParts.length === msg.parts.length) return msg;
return { ...msg, parts: cleanedParts };
})
.filter((msg) => msg.role !== "assistant" || msg.parts.length > 0);


// Sync changes to the database
await Promise.all(
messagesFromDb.map(async (dbMsg) => {
const parts = dbMsg.parts as Record<string, unknown>[];
if (!parts.some(isPendingApproval)) return;
await updateMessage({
id: dbMsg.id,
parts: parts.map((p) =>
isPendingApproval(p) ? { ...p, state: "output-denied" } : p,
) as DBMessage["parts"],
});
}),
);

const modelMessages = await convertToModelMessages(cleanedMessages);

const stream = createUIMessageStream({
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
Expand Down
45 changes: 40 additions & 5 deletions hooks/use-active-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DefaultChatTransport } from "ai";
import { usePathname } from "next/navigation";
import {
createContext,
useCallback,
type Dispatch,
type ReactNode,
type SetStateAction,
Expand Down Expand Up @@ -117,8 +118,7 @@ export function ActiveChatProvider({ children }: { children: ReactNode }) {
(part) =>
"state" in part &&
part.state === "approval-responded" &&
"approval" in part &&
(part.approval as { approved?: boolean })?.approved === true
"approval" in part
) ?? false
);
},
Expand All @@ -128,7 +128,7 @@ export function ActiveChatProvider({ children }: { children: ReactNode }) {
prepareSendMessagesRequest(request) {
const lastMessage = request.messages.at(-1);
const isToolApprovalContinuation =
lastMessage?.role !== "user" ||
lastMessage?.role !== "user" &&
request.messages.some((msg) =>
msg.parts?.some((part) => {
const state = (part as { state?: string }).state;
Expand Down Expand Up @@ -244,12 +244,47 @@ export function ActiveChatProvider({ children }: { children: ReactNode }) {
{ revalidateOnFocus: false }
);

// Automatically change all messages still in the 'approval-requested' state to 'output-denied' when sending a new message.
// Ensure the user can continue chatting even without explicit approval (neither 'Allow' nor 'Deny').
const wrappedSendMessage: typeof sendMessage = useCallback(
(...args) => {
setMessages((prev) =>
prev.map((msg) => {
if (msg.role !== "assistant") return msg;

const hasPending = msg.parts.some(
(part) =>
"state" in part &&
(part as Record<string, unknown>).state === "approval-requested",
);
if (!hasPending) return msg;

return {
...msg,
parts: msg.parts.map((part) => {
if (
"state" in part &&
(part as Record<string, unknown>).state === "approval-requested"
) {
return { ...part, state: "output-denied" } as typeof part;
}
return part;
}),
};
}),
);

return sendMessage(...args);
},
[sendMessage, setMessages],
);

const value = useMemo<ActiveChatContextValue>(
() => ({
chatId,
messages,
setMessages,
sendMessage,
sendMessage: wrappedSendMessage,
status,
stop,
regenerate,
Expand All @@ -269,7 +304,7 @@ export function ActiveChatProvider({ children }: { children: ReactNode }) {
chatId,
messages,
setMessages,
sendMessage,
wrappedSendMessage,
status,
stop,
regenerate,
Expand Down