feat(frontend): implement V1 conversation pause/resume functionality (#11541)

This commit is contained in:
sp.wack
2025-10-28 19:45:40 +04:00
committed by GitHub
parent bc8922d3f9
commit fc67f39b74
6 changed files with 178 additions and 13 deletions

View File

@@ -198,6 +198,34 @@ class V1ConversationService {
return data;
}
/**
* Resume a V1 conversation
* Uses the custom runtime URL from the conversation
*
* @param conversationId The conversation ID
* @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
* @param sessionApiKey Session API key for authentication (required for V1)
* @returns Success response
*/
static async resumeConversation(
conversationId: string,
conversationUrl: string | null | undefined,
sessionApiKey?: string | null,
): Promise<{ success: boolean }> {
const url = this.buildRuntimeUrl(
conversationUrl,
`/api/conversations/${conversationId}/run`,
);
const headers = this.buildSessionHeaders(sessionApiKey);
const { data } = await axios.post<{ success: boolean }>(
url,
{},
{ headers },
);
return data;
}
/**
* Pause a V1 sandbox
* Calls the /api/v1/sandboxes/{id}/pause endpoint

View File

@@ -10,6 +10,8 @@ import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { useSendMessage } from "#/hooks/use-send-message";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { AgentState } from "#/types/agent-state";
import { useV1PauseConversation } from "#/hooks/mutation/use-v1-pause-conversation";
import { useV1ResumeConversation } from "#/hooks/mutation/use-v1-resume-conversation";
interface ChatInputActionsProps {
conversationStatus: ConversationStatus | null;
@@ -26,6 +28,8 @@ export function ChatInputActions({
const pauseConversationSandboxMutation = useUnifiedPauseConversationSandbox();
const resumeConversationSandboxMutation =
useUnifiedResumeConversationSandbox();
const v1PauseConversationMutation = useV1PauseConversation();
const v1ResumeConversationMutation = useV1ResumeConversation();
const { conversationId } = useConversationId();
const { providers } = useUserProviders();
const { send } = useSendMessage();
@@ -38,7 +42,8 @@ export function ChatInputActions({
const handlePauseAgent = () => {
if (isV1Conversation) {
// V1: Empty function for now
// V1: Pause the conversation (agent execution)
v1PauseConversationMutation.mutate({ conversationId });
return;
}
@@ -46,11 +51,24 @@ export function ChatInputActions({
send(generateAgentStateChangeEvent(AgentState.STOPPED));
};
const handleResumeAgentClick = () => {
if (isV1Conversation) {
// V1: Resume the conversation (agent execution)
v1ResumeConversationMutation.mutate({ conversationId });
return;
}
// V0: Call the original handleResumeAgent (sends "continue" message)
handleResumeAgent();
};
const handleStartClick = () => {
resumeConversationSandboxMutation.mutate({ conversationId, providers });
};
const isPausing = pauseConversationSandboxMutation.isPending;
const isPausing =
pauseConversationSandboxMutation.isPending ||
v1PauseConversationMutation.isPending;
return (
<div className="w-full flex items-center justify-between">
@@ -66,7 +84,7 @@ export function ChatInputActions({
<AgentStatus
className="ml-2 md:ml-3"
handleStop={handlePauseAgent}
handleResumeAgent={handleResumeAgent}
handleResumeAgent={handleResumeAgentClick}
disabled={disabled}
isPausing={isPausing}
/>

View File

@@ -74,19 +74,24 @@ export function AgentStatus({
<div
className={cn(
"bg-[#525252] box-border content-stretch flex flex-row gap-[3px] items-center justify-center overflow-clip px-0.5 py-1 relative rounded-[100px] shrink-0 size-6 transition-all duration-200 active:scale-95",
(shouldShownAgentStop || shouldShownAgentResume) &&
!shouldShownAgentLoading &&
(shouldShownAgentStop || shouldShownAgentResume) &&
"hover:bg-[#737373] cursor-pointer",
)}
>
{shouldShownAgentLoading && <AgentLoading />}
{shouldShownAgentStop && <ChatStopButton handleStop={handleStop} />}
{shouldShownAgentResume && (
{!shouldShownAgentLoading && shouldShownAgentStop && (
<ChatStopButton handleStop={handleStop} />
)}
{!shouldShownAgentLoading && shouldShownAgentResume && (
<ChatResumeAgentButton
onAgentResumed={handleResumeAgent}
disabled={disabled}
/>
)}
{shouldShownAgentError && <CircleErrorIcon className="w-4 h-4" />}
{!shouldShownAgentLoading && shouldShownAgentError && (
<CircleErrorIcon className="w-4 h-4" />
)}
{!shouldShownAgentLoading &&
!shouldShownAgentStop &&
!shouldShownAgentResume &&

View File

@@ -18,11 +18,15 @@ export const getConversationVersionFromQueryCache = (
};
/**
* Fetches a V1 conversation's sandbox_id
* Fetches a V1 conversation's sandbox_id and conversation_url
*/
const fetchV1ConversationSandboxId = async (
const fetchV1ConversationData = async (
conversationId: string,
): Promise<string> => {
): Promise<{
sandboxId: string;
conversationUrl: string | null;
sessionApiKey: string | null;
}> => {
const conversations = await V1ConversationService.batchGetAppConversations([
conversationId,
]);
@@ -32,17 +36,34 @@ const fetchV1ConversationSandboxId = async (
throw new Error(`V1 conversation not found: ${conversationId}`);
}
return appConversation.sandbox_id;
return {
sandboxId: appConversation.sandbox_id,
conversationUrl: appConversation.conversation_url,
sessionApiKey: appConversation.session_api_key,
};
};
/**
* Pause a V1 conversation sandbox by fetching the sandbox_id and pausing it
*/
export const pauseV1ConversationSandbox = async (conversationId: string) => {
const sandboxId = await fetchV1ConversationSandboxId(conversationId);
const { sandboxId } = await fetchV1ConversationData(conversationId);
return V1ConversationService.pauseSandbox(sandboxId);
};
/**
* Pause a V1 conversation by fetching the conversation data and pausing it
*/
export const pauseV1Conversation = async (conversationId: string) => {
const { conversationUrl, sessionApiKey } =
await fetchV1ConversationData(conversationId);
return V1ConversationService.pauseConversation(
conversationId,
conversationUrl,
sessionApiKey,
);
};
/**
* Stops a V0 conversation using the legacy API
*/
@@ -53,10 +74,23 @@ export const stopV0Conversation = async (conversationId: string) =>
* Resumes a V1 conversation sandbox by fetching the sandbox_id and resuming it
*/
export const resumeV1ConversationSandbox = async (conversationId: string) => {
const sandboxId = await fetchV1ConversationSandboxId(conversationId);
const { sandboxId } = await fetchV1ConversationData(conversationId);
return V1ConversationService.resumeSandbox(sandboxId);
};
/**
* Resume a V1 conversation by fetching the conversation data and resuming it
*/
export const resumeV1Conversation = async (conversationId: string) => {
const { conversationUrl, sessionApiKey } =
await fetchV1ConversationData(conversationId);
return V1ConversationService.resumeConversation(
conversationId,
conversationUrl,
sessionApiKey,
);
};
/**
* Starts a V0 conversation using the legacy API
*/

View File

@@ -0,0 +1,40 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { pauseV1Conversation } from "./conversation-mutation-utils";
export const useV1PauseConversation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { conversationId: string }) =>
pauseV1Conversation(variables.conversationId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ["user", "conversations"] });
const previousConversations = queryClient.getQueryData([
"user",
"conversations",
]);
return { previousConversations };
},
onError: (_, __, context) => {
if (context?.previousConversations) {
queryClient.setQueryData(
["user", "conversations"],
context.previousConversations,
);
}
},
onSettled: (_, __, variables) => {
// Invalidate the specific conversation query to trigger automatic refetch
queryClient.invalidateQueries({
queryKey: ["user", "conversation", variables.conversationId],
});
// Also invalidate the conversations list for consistency
queryClient.invalidateQueries({ queryKey: ["user", "conversations"] });
// Invalidate V1 batch get queries
queryClient.invalidateQueries({
queryKey: ["v1-batch-get-app-conversations"],
});
},
});
};

View File

@@ -0,0 +1,40 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { resumeV1Conversation } from "./conversation-mutation-utils";
export const useV1ResumeConversation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { conversationId: string }) =>
resumeV1Conversation(variables.conversationId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ["user", "conversations"] });
const previousConversations = queryClient.getQueryData([
"user",
"conversations",
]);
return { previousConversations };
},
onError: (_, __, context) => {
if (context?.previousConversations) {
queryClient.setQueryData(
["user", "conversations"],
context.previousConversations,
);
}
},
onSettled: (_, __, variables) => {
// Invalidate the specific conversation query to trigger automatic refetch
queryClient.invalidateQueries({
queryKey: ["user", "conversation", variables.conversationId],
});
// Also invalidate the conversations list for consistency
queryClient.invalidateQueries({ queryKey: ["user", "conversations"] });
// Invalidate V1 batch get queries
queryClient.invalidateQueries({
queryKey: ["v1-batch-get-app-conversations"],
});
},
});
};