feat(frontend): update stop sandbox dialog to display conversations in sandbox (#13388)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell
2026-03-16 05:20:10 -06:00
committed by GitHub
parent d591b140c8
commit aec95ecf3b
8 changed files with 155 additions and 44 deletions

View File

@@ -72,7 +72,7 @@ vi.mock("react-i18next", async () => {
CONVERSATION$SHOW_SKILLS: "Show Skills",
BUTTON$DISPLAY_COST: "Display Cost",
COMMON$CLOSE_CONVERSATION_STOP_RUNTIME:
"Close Conversation (Stop Runtime)",
"Close Conversation (Stop Sandbox)",
COMMON$DELETE_CONVERSATION: "Delete Conversation",
CONVERSATION$SHARE_PUBLICLY: "Share Publicly",
CONVERSATION$LINK_COPIED: "Link copied to clipboard",
@@ -565,7 +565,7 @@ describe("ConversationNameContextMenu", () => {
"Delete Conversation",
);
expect(screen.getByTestId("stop-button")).toHaveTextContent(
"Close Conversation (Stop Runtime)",
"Close Conversation (Stop Sandbox)",
);
expect(screen.getByTestId("display-cost-button")).toHaveTextContent(
"Display Cost",

View File

@@ -12,6 +12,7 @@ import type {
V1AppConversationStartTask,
V1AppConversationStartTaskPage,
V1AppConversation,
V1AppConversationPage,
GetSkillsResponse,
V1RuntimeConversationInfo,
} from "./v1-conversation-service.types";
@@ -424,6 +425,28 @@ class V1ConversationService {
});
return data;
}
/**
* Search for V1 conversations by sandbox ID
*
* @param sandboxId The sandbox ID to filter by
* @param limit Maximum number of results (default: 100)
* @returns Array of conversations in the specified sandbox
*/
static async searchConversationsBySandboxId(
sandboxId: string,
limit: number = 100,
): Promise<V1AppConversation[]> {
const params = new URLSearchParams();
params.append("sandbox_id__eq", sandboxId);
params.append("limit", limit.toString());
const { data } = await openHands.get<V1AppConversationPage>(
`/api/v1/app-conversations/search?${params.toString()}`,
);
return data.items;
}
}
export default V1ConversationService;

View File

@@ -119,6 +119,11 @@ export interface V1AppConversation {
public?: boolean;
}
export interface V1AppConversationPage {
items: V1AppConversation[];
next_page_id: string | null;
}
export interface Skill {
name: string;
type: "repo" | "knowledge" | "agentskills";

View File

@@ -7,17 +7,71 @@ import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { BrandButton } from "../settings/brand-button";
import { I18nKey } from "#/i18n/declaration";
import { useConversationsInSandbox } from "#/hooks/query/use-conversations-in-sandbox";
interface ConfirmStopModalProps {
onConfirm: () => void;
onCancel: () => void;
sandboxId: string | null;
}
function ConversationsList({
conversations,
isLoading,
isError,
t,
}: {
conversations: { id: string; title: string | null }[] | undefined;
isLoading: boolean;
isError: boolean;
t: (key: string) => string;
}) {
if (isLoading) {
return (
<div
className="text-sm text-content-secondary"
data-testid="conversations-loading"
>
{t(I18nKey.HOME$LOADING)}
</div>
);
}
if (isError) {
return (
<div className="text-sm text-danger" data-testid="conversations-error">
{t(I18nKey.COMMON$ERROR)}
</div>
);
}
if (conversations && conversations.length > 0) {
return (
<ul
className="list-disc list-inside text-sm text-content-secondary"
data-testid="conversations-list"
>
{conversations.map((conv) => (
<li key={conv.id}>{conv.title || conv.id}</li>
))}
</ul>
);
}
return null;
}
export function ConfirmStopModal({
onConfirm,
onCancel,
sandboxId,
}: ConfirmStopModalProps) {
const { t } = useTranslation();
const {
data: conversations,
isLoading,
isError,
} = useConversationsInSandbox(sandboxId);
return (
<ModalBackdrop onClose={onCancel}>
@@ -29,6 +83,12 @@ export function ConfirmStopModal({
<BaseModalDescription
description={t(I18nKey.CONVERSATION$CLOSE_CONVERSATION_WARNING)}
/>
<ConversationsList
conversations={conversations}
isLoading={isLoading}
isError={isError}
t={t}
/>
</div>
<div
className="flex flex-col gap-2 w-full"

View File

@@ -44,6 +44,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
React.useState<string | null>(null);
const [selectedConversationVersion, setSelectedConversationVersion] =
React.useState<"V0" | "V1" | undefined>(undefined);
const [selectedSandboxId, setSelectedSandboxId] = React.useState<
string | null
>(null);
const [openContextMenuId, setOpenContextMenuId] = React.useState<
string | null
>(null);
@@ -85,10 +88,12 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
const handleStopConversation = (
conversationId: string,
version?: "V0" | "V1",
sandboxId?: string | null,
) => {
setConfirmStopModalVisible(true);
setSelectedConversationId(conversationId);
setSelectedConversationVersion(version);
setSelectedSandboxId(sandboxId ?? null);
};
const handleConversationTitleChange = async (
@@ -185,6 +190,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
handleStopConversation(
project.conversation_id,
project.conversation_version,
project.sandbox_id,
)
}
onChangeTitle={(title) =>
@@ -238,6 +244,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
setConfirmStopModalVisible(false);
}}
onCancel={() => setConfirmStopModalVisible(false)}
sandboxId={selectedSandboxId}
/>
)}

View File

@@ -233,6 +233,7 @@ export function ConversationName() {
<ConfirmStopModal
onConfirm={handleConfirmStop}
onCancel={() => setConfirmStopModalVisible(false)}
sandboxId={conversation?.sandbox_id ?? null}
/>
)}
</>

View File

@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
export const useConversationsInSandbox = (sandboxId: string | null) =>
useQuery({
queryKey: ["conversations", "sandbox", sandboxId],
queryFn: () =>
sandboxId
? V1ConversationService.searchConversationsBySandboxId(sandboxId)
: Promise.resolve([]),
enabled: !!sandboxId,
staleTime: 0, // Always consider data stale for confirmation dialogs
gcTime: 1000 * 60, // 1 minute
refetchOnMount: true, // Always fetch fresh data when modal opens
});

View File

@@ -5856,36 +5856,36 @@
"uk": "Ви впевнені, що хочете призупинити цю розмову?"
},
"CONVERSATION$CONFIRM_CLOSE_CONVERSATION": {
"en": "Confirm Close Conversation",
"ja": "会話終了の確認",
"zh-CN": "确认关闭对话",
"zh-TW": "確認關閉對話",
"ko-KR": "대화 종료 확인",
"no": "Bekreft avslutt samtale",
"it": "Conferma chiusura conversazione",
"pt": "Confirmar encerrar conversa",
"es": "Confirmar cerrar conversación",
"ar": "تأكيد إغلاق المحادثة",
"fr": "Confirmer la fermeture de la conversation",
"tr": "Konuşmayı Kapatmayı Onayla",
"de": "Gespräch schließen bestätigen",
"uk": "Підтвердити закриття розмови"
"en": "Confirm Stop Sandbox",
"ja": "サンドボックス停止の確認",
"zh-CN": "确认停止沙盒",
"zh-TW": "確認停止沙盒",
"ko-KR": "샌드박스 중지 확인",
"no": "Bekreft stopp sandkasse",
"it": "Conferma arresto sandbox",
"pt": "Confirmar parar sandbox",
"es": "Confirmar detener sandbox",
"ar": "تأكيد إيقاف صندوق الحماية",
"fr": "Confirmer l'arrêt du sandbox",
"tr": "Sandbox'ı Durdurmayı Onayla",
"de": "Sandbox-Stopp bestätigen",
"uk": "Підтвердити зупинку пісочниці"
},
"CONVERSATION$CLOSE_CONVERSATION_WARNING": {
"en": "Are you sure you want to close this conversation and stop the runtime?",
"ja": "この会話を終了してランタイムを停止してもよろしいですか?",
"zh-CN": "您确定要关闭此对话并停止运行时吗?",
"zh-TW": "您確定要關閉此對話並停止執行時嗎?",
"ko-KR": "이 대화를 종료하고 런타임을 중지하시겠습니까?",
"no": "Er du sikker på at du vil avslutte denne samtalen og stoppe kjøretiden?",
"it": "Sei sicuro di voler chiudere questa conversazione e fermare il runtime?",
"pt": "Tem certeza de que deseja encerrar esta conversa e parar o runtime?",
"es": "¿Está seguro de que desea cerrar esta conversación y detener el runtime?",
"ar": "هل أنت متأكد أنك تريد إغلاق هذه المحادثة وإيقاف وقت التشغيل؟",
"fr": "Êtes-vous sûr de vouloir fermer cette conversation et arrêter le runtime ?",
"tr": "Bu konuşmayı kapatmak ve çalışma zamanını durdurmak istediğinizden emin misiniz?",
"de": "Sind Sie sicher, dass Sie dieses Gespräch schließen und die Laufzeit stoppen möchten?",
"uk": "Ви впевнені, що хочете закрити цю розмову та зупинити час виконання?"
"en": "This will stop the sandbox, and pause the following conversations:",
"ja": "サンドボックスを停止し、以下の会話を一時停止します:",
"zh-CN": "这将停止沙盒,并暂停以下对话:",
"zh-TW": "這將停止沙盒,並暫停以下對話:",
"ko-KR": "샌드박스를 중지하고 다음 대화를 일시 중지합니다:",
"no": "Dette vil stoppe sandkassen og pause følgende samtaler:",
"it": "Questo fermerà la sandbox e metterà in pausa le seguenti conversazioni:",
"pt": "Isso irá parar o sandbox e pausar as seguintes conversas:",
"es": "Esto detendrá el sandbox y pausará las siguientes conversaciones:",
"ar": "سيؤدي هذا إلى إيقاف صندوق الحماية وإيقاف المحادثات التالية مؤقتًا:",
"fr": "Cela arrêtera le sandbox et mettra en pause les conversations suivantes :",
"tr": "Bu, sandbox'ı durduracak ve aşağıdaki konuşmaları duraklatacaktır:",
"de": "Dies wird die Sandbox stoppen und die folgenden Gespräche pausieren:",
"uk": "Це зупинить пісочницю та призупинить наступні розмови:"
},
"CONVERSATION$STOP_WARNING": {
"en": "Are you sure you want to pause this conversation?",
@@ -14964,20 +14964,20 @@
"uk": "Натисніть тут"
},
"COMMON$CLOSE_CONVERSATION_STOP_RUNTIME": {
"en": "Close Conversation (Stop Runtime)",
"ja": "会話を閉じる(ランタイム停止)",
"zh-CN": "关闭对话(停止运行时",
"zh-TW": "關閉對話(停止執行時",
"ko-KR": "대화 닫기(런타임 중지)",
"no": "Lukk samtale (stopp kjøring)",
"it": "Chiudi conversazione (Interrompi runtime)",
"pt": "Fechar conversa (Parar execução)",
"es": "Cerrar conversación (Detener ejecución)",
"ar": "إغلاق المحادثة (إيقاف وقت التشغيل)",
"fr": "Fermer la conversation (Arrêter l'exécution)",
"tr": "Konuşmayı Kapat (Çalışma Zamanını Durdur)",
"de": "Gespräch schließen (Laufzeit beenden)",
"uk": "Закрити розмову (зупинити виконання)"
"en": "Close Conversation (Stop Sandbox)",
"ja": "会話を閉じる(サンドボックス停止)",
"zh-CN": "关闭对话(停止沙盒",
"zh-TW": "關閉對話(停止沙盒",
"ko-KR": "대화 닫기(샌드박스 중지)",
"no": "Lukk samtale (stopp sandkasse)",
"it": "Chiudi conversazione (Interrompi sandbox)",
"pt": "Fechar conversa (Parar sandbox)",
"es": "Cerrar conversación (Detener sandbox)",
"ar": "إغلاق المحادثة (إيقاف صندوق الحماية)",
"fr": "Fermer la conversation (Arrêter le sandbox)",
"tr": "Konuşmayı Kapat (Sandbox'ı Durdur)",
"de": "Gespräch schließen (Sandbox beenden)",
"uk": "Закрити розмову (зупинити пісочницю)"
},
"COMMON$CODE": {
"en": "Code",