diff --git a/frontend/__tests__/components/features/conversation/conversation-name.test.tsx b/frontend/__tests__/components/features/conversation/conversation-name.test.tsx index 45716775cc..3152fde699 100644 --- a/frontend/__tests__/components/features/conversation/conversation-name.test.tsx +++ b/frontend/__tests__/components/features/conversation/conversation-name.test.tsx @@ -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", diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts index 17cbb24cdf..30fdeb9369 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts @@ -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 { + const params = new URLSearchParams(); + params.append("sandbox_id__eq", sandboxId); + params.append("limit", limit.toString()); + + const { data } = await openHands.get( + `/api/v1/app-conversations/search?${params.toString()}`, + ); + + return data.items; + } } export default V1ConversationService; diff --git a/frontend/src/api/conversation-service/v1-conversation-service.types.ts b/frontend/src/api/conversation-service/v1-conversation-service.types.ts index fb59623372..b437e17bf1 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.types.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.types.ts @@ -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"; diff --git a/frontend/src/components/features/conversation-panel/confirm-stop-modal.tsx b/frontend/src/components/features/conversation-panel/confirm-stop-modal.tsx index d841211ace..acf30f4b09 100644 --- a/frontend/src/components/features/conversation-panel/confirm-stop-modal.tsx +++ b/frontend/src/components/features/conversation-panel/confirm-stop-modal.tsx @@ -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 ( +
+ {t(I18nKey.HOME$LOADING)} +
+ ); + } + + if (isError) { + return ( +
+ {t(I18nKey.COMMON$ERROR)} +
+ ); + } + + if (conversations && conversations.length > 0) { + return ( + + ); + } + + return null; } export function ConfirmStopModal({ onConfirm, onCancel, + sandboxId, }: ConfirmStopModalProps) { const { t } = useTranslation(); + const { + data: conversations, + isLoading, + isError, + } = useConversationsInSandbox(sandboxId); return ( @@ -29,6 +83,12 @@ export function ConfirmStopModal({ +
(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} /> )} diff --git a/frontend/src/components/features/conversation/conversation-name.tsx b/frontend/src/components/features/conversation/conversation-name.tsx index b7a26aad30..664c583839 100644 --- a/frontend/src/components/features/conversation/conversation-name.tsx +++ b/frontend/src/components/features/conversation/conversation-name.tsx @@ -233,6 +233,7 @@ export function ConversationName() { setConfirmStopModalVisible(false)} + sandboxId={conversation?.sandbox_id ?? null} /> )} diff --git a/frontend/src/hooks/query/use-conversations-in-sandbox.ts b/frontend/src/hooks/query/use-conversations-in-sandbox.ts new file mode 100644 index 0000000000..f41edb7a54 --- /dev/null +++ b/frontend/src/hooks/query/use-conversations-in-sandbox.ts @@ -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 + }); diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index d3f91ceec7..3437fde4a6 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -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",