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
8 changes: 4 additions & 4 deletions components/VercelChat/message-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { useVercelChatContext } from "@/providers/VercelChatProvider";
import { TextUIPart, UIMessage } from "ai";

export function MessageEditor({ message, setMode }: EditingMessageProps) {
const { id, setMessages, reload } = useVercelChatContext();
if (!id) {
const { transportChatId, setMessages, reload } = useVercelChatContext();
if (!transportChatId) {
throw new Error("MessageEditor requires an active chat id");
}
const text = (message.parts[0] as TextUIPart)?.text || "";
Expand All @@ -31,7 +31,7 @@ export function MessageEditor({ message, setMode }: EditingMessageProps) {
});
setMode("view");
if (text !== draftContent) {
reload();
void reload({ skipTrailingDelete: true });
}
},
});
Expand Down Expand Up @@ -83,7 +83,7 @@ export function MessageEditor({ message, setMode }: EditingMessageProps) {
disabled={isDeletingTrailingMessages}
onClick={() => {
void deleteTrailingMessages({
chatId: id,
chatId: transportChatId,
fromMessageId: message.id,
});
}}
Expand Down
5 changes: 5 additions & 0 deletions hooks/reloadOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Options for {@link useVercelChat}'s reload (retry / edit-and-resubmit). */
export type ReloadOptions = {
/** When true, skip server trailing delete (caller already deleted). */
skipTrailingDelete?: boolean;
};
61 changes: 55 additions & 6 deletions hooks/useVercelChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import { usePrivy } from "@privy-io/react-auth";
import { TextAttachment } from "@/types/textAttachment";
import { formatTextAttachments } from "@/lib/chat/formatTextAttachments";
import { useDeleteTrailingMessages } from "./useDeleteTrailingMessages";
import {
deleteTrailingMessages as deleteTrailingMessagesApi,
TrailingDeleteError,
} from "@/lib/messages/deleteTrailingMessages";
import type { ReloadOptions } from "./reloadOptions";
import { getFileContents } from "@/lib/sandboxes/getFileContents";
import getMimeFromPath from "@/lib/files/getMimeFromPath";
import { getChatPath } from "@/lib/chat/getChatPath";
Expand Down Expand Up @@ -301,14 +306,57 @@ export function useVercelChat({
sendMessage(message, { body: chatRequestBody, headers });
};

const handleReload = useCallback(async () => {
const handleReload = useCallback(async (options?: ReloadOptions) => {
const headers = await getHeaders();
// Retry/Edit (MessageParts + message-editor) call reload() → regenerate,
// bypassing handleSubmit; persist the selected model first so the
// regenerated turn bills it, not the previous/default model.
// MessageParts Retry calls reload() to delete the assistant tail then
// regenerate. MessageEditor already deleted trailing server-side and
// passes skipTrailingDelete (messages state may still list the assistant).
await persistSelectedModel();

if (!options?.skipTrailingDelete) {
const lastMessage = messages.at(-1);
if (lastMessage?.role === "assistant") {
const accessToken = await getAccessToken();
if (!accessToken) {
toast.error("Please sign in to regenerate.");
return;
}

try {
await deleteTrailingMessagesApi({
chatId: transportChatId,
fromMessageId: lastMessage.id,
accessToken,
});
} catch (error) {
// Boundary already removed (e.g. race) — still regenerate.
if (
error instanceof TrailingDeleteError &&
error.status === 404
) {
// no-op
} else {
console.error(
"Failed to delete trailing messages before regenerate:",
error,
);
toast.error("Failed to regenerate. Please try again.");
return;
}
}
}
}

await regenerate({ body: chatRequestBody, headers });
}, [getHeaders, regenerate, chatRequestBody, persistSelectedModel]);
}, [
getHeaders,
getAccessToken,
regenerate,
chatRequestBody,
persistSelectedModel,
messages,
transportChatId,
]);

// Keep messagesRef in sync with messages
messagesLengthRef.current = messages.length;
Expand Down Expand Up @@ -344,7 +392,7 @@ export function useVercelChat({

if (earliestFailedUserMessageId) {
await deleteTrailingMessages({
chatId: id,
chatId: transportChatId,
fromMessageId: earliestFailedUserMessageId,
});
}
Expand Down Expand Up @@ -403,6 +451,7 @@ export function useVercelChat({
return {
// States
messages,
transportChatId,
status,
input,
isLoading,
Expand Down
15 changes: 14 additions & 1 deletion lib/messages/deleteTrailingMessages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl";

export class TrailingDeleteError extends Error {
readonly status: number;

constructor(message: string, status: number) {
super(message);
this.name = "TrailingDeleteError";
this.status = status;
}
}

/**
* Deletes trailing messages in a chat from a given message ID onward.
*/
Expand All @@ -24,6 +34,9 @@ export async function deleteTrailingMessages({
);

if (!response.ok) {
throw new Error("Failed to delete trailing messages");
throw new TrailingDeleteError(
"Failed to delete trailing messages",
response.status,
);
}
}
14 changes: 10 additions & 4 deletions providers/VercelChatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import { useArtistProvider } from "./ArtistProvider";
import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";
import { TextAttachment } from "@/types/textAttachment";
import type { WorkspaceStatus } from "@/components/VercelChat/WorkspaceStatusIndicator";
import type { ReloadOptions } from "@/hooks/reloadOptions";

// Interface for the context data
interface VercelChatContextType {
id: string | undefined;
/** Api-facing chat id (`workflowChatId ?? id`) for recoup-api calls. */
transportChatId: string;
sessionId: string | undefined;
messages: UIMessage[];
availableModels: GatewayLanguageModelEntry[];
Expand All @@ -31,7 +34,7 @@ interface VercelChatContextType {
setInput: (input: string) => void;
input: string;
setMessages: UseChatHelpers<UIMessage>["setMessages"];
reload: () => void;
reload: (options?: ReloadOptions) => Promise<void>;
append: (message: UIMessage) => void;
attachments: FileUIPart[];
pendingAttachments: FileUIPart[];
Expand Down Expand Up @@ -114,6 +117,7 @@ export function VercelChatProvider({
// Use the useVercelChat hook to get the chat state and functions
const {
messages,
transportChatId,
status,
isLoading,
hasError,
Expand All @@ -138,9 +142,10 @@ export function VercelChatProvider({
textAttachments,
});

const reload = useCallback(() => {
return originalReload();
}, [originalReload]);
const reload = useCallback(
(options?: ReloadOptions) => originalReload(options),
[originalReload],
);

// When a message is sent successfully, clear the attachments
const handleSendMessageWithClear = async (
Expand All @@ -155,6 +160,7 @@ export function VercelChatProvider({
// Create the context value object
const contextValue: VercelChatContextType = {
id: chatId,
transportChatId,
sessionId,
messages,
model,
Expand Down
Loading