mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 23:38:08 -05:00
feat(frontend): implement V1 conversation pause/resume functionality (#11541)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
40
frontend/src/hooks/mutation/use-v1-pause-conversation.ts
Normal file
40
frontend/src/hooks/mutation/use-v1-pause-conversation.ts
Normal 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"],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
40
frontend/src/hooks/mutation/use-v1-resume-conversation.ts
Normal file
40
frontend/src/hooks/mutation/use-v1-resume-conversation.ts
Normal 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"],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user