فهرست منبع

(enh) send status messages to UI during startup (#3771)

Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Robert Brennan <contact@rbren.io>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
tobitege 1 سال پیش
والد
کامیت
c32cec7f89

+ 1 - 0
.gitignore

@@ -228,3 +228,4 @@ runtime_*.tar
 # docker build
 containers/runtime/Dockerfile
 containers/runtime/project.tar.gz
+containers/runtime/code

+ 5 - 4
containers/runtime/README.md

@@ -1,11 +1,12 @@
-# Dynamic constructed Dockerfile
+# Dynamically constructed Dockerfile
 
-This folder builds runtime image (sandbox), which will use a `Dockerfile` that is dynamically generated depends on the `base_image` AND a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that's based on the current commit of `openhands`.
+This folder builds a runtime image (sandbox), which will use a dynamically generated `Dockerfile`
+that depends on the `base_image` **AND** a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that is based on the current commit of `openhands`.
 
-The following command will generate Dockerfile for `ubuntu:22.04` and the source distribution `.tar` into `containers/runtime`.
+The following command will generate a `Dockerfile` file for `nikolaik/python-nodejs:python3.11-nodejs22` (the default base image), an updated `config.sh` and the runtime source distribution files/folders into `containers/runtime`:
 
 ```bash
 poetry run python3 openhands/runtime/utils/runtime_build.py \
-    --base_image ubuntu:22.04 \
+    --base_image nikolaik/python-nodejs:python3.11-nodejs22 \
     --build_folder containers/runtime
 ```

+ 19 - 7
frontend/src/components/AgentStatusBar.tsx

@@ -18,6 +18,7 @@ enum IndicatorColor {
 function AgentStatusBar() {
   const { t } = useTranslation();
   const { curAgentState } = useSelector((state: RootState) => state.agent);
+  const { curStatusMessage } = useSelector((state: RootState) => state.status);
 
   const AgentStatusMap: {
     [k: string]: { message: string; indicator: IndicatorColor };
@@ -90,14 +91,25 @@ function AgentStatusBar() {
     }
   }, [curAgentState]);
 
+  const [statusMessage, setStatusMessage] = React.useState<string>("");
+
+  React.useEffect(() => {
+    const trimmedCustomMessage = curStatusMessage.message.trim();
+    if (trimmedCustomMessage) {
+      setStatusMessage(t(trimmedCustomMessage));
+    } else {
+      setStatusMessage(AgentStatusMap[curAgentState].message);
+    }
+  }, [curAgentState, curStatusMessage.message]);
+
   return (
-    <div className="flex items-center">
-      <div
-        className={`w-3 h-3 mr-2 rounded-full animate-pulse ${AgentStatusMap[curAgentState].indicator}`}
-      />
-      <span className="text-sm text-stone-400">
-        {AgentStatusMap[curAgentState].message}
-      </span>
+    <div className="flex flex-col items-center">
+      <div className="flex items-center">
+        <div
+          className={`w-3 h-3 mr-2 rounded-full animate-pulse ${AgentStatusMap[curAgentState].indicator}`}
+        />
+        <span className="text-sm text-stone-400">{statusMessage}</span>
+      </div>
     </div>
   );
 }

+ 774 - 85
frontend/src/i18n/translation.json

@@ -72,19 +72,43 @@
     "en": "Options",
     "zh-CN": "选项",
     "zh-TW": "選項",
-    "de": "Optionen"
+    "de": "Optionen",
+    "ko-KR": "옵션",
+    "no": "Alternativer",
+    "it": "Opzioni",
+    "pt": "Opções",
+    "es": "Opciones",
+    "ar": "خيارات",
+    "fr": "Options",
+    "tr": "Seçenekler"
   },
   "CODE_EDITOR$FILE_SAVE_ERROR": {
     "en": "An unknown error occurred while saving the file",
     "zh-CN": "文件保存时发生未知错误",
     "zh-TW": "文件保存時發生未知錯誤",
-    "de": "Beim Speichern der Datei ist ein unbekannter Fehler aufgetreten"
+    "de": "Beim Speichern der Datei ist ein unbekannter Fehler aufgetreten",
+    "ko-KR": "파일 저장 중 알 수 없는 오류가 발생했습니다",
+    "no": "En ukjent feil oppstod under lagring av filen",
+    "it": "Si è verificato un errore sconosciuto durante il salvataggio del file",
+    "pt": "Ocorreu um erro desconhecido ao salvar o arquivo",
+    "es": "Ocurrió un error desconocido al guardar el archivo",
+    "ar": "حدث خطأ غير معروف أثناء حفظ الملف",
+    "fr": "Une erreur inconnue s'est produite lors de l'enregistrement du fichier",
+    "tr": "Dosya kaydedilirken bilinmeyen bir hata oluştu"
   },
   "CODE_EDITOR$EMPTY_MESSAGE": {
     "en": "No file selected.",
     "zh-CN": "文件未选中",
     "zh-TW": "未選取任何文件。",
-    "de": "Keine Datei ausgewählt."
+    "de": "Keine Datei ausgewählt.",
+    "ko-KR": "선택된 파일이 없습니다.",
+    "no": "Ingen fil valgt.",
+    "it": "Nessun file selezionato.",
+    "pt": "Nenhum arquivo selecionado.",
+    "es": "Ningún archivo seleccionado.",
+    "ar": "لم يتم اختيار أي ملف.",
+    "fr": "Aucun fichier sélectionné.",
+    "tr": "Hiçbir dosya seçilmedi."
   },
   "FILE_SERVICE$SELECT_FILE_ERROR": {
     "en": "Error selecting file. Please try again.",
@@ -336,12 +360,30 @@
   "CONFIGURATION$SECURITY_SELECT_LABEL": {
     "en": "Security analyzer",
     "de": "Sicherheitsanalysator",
-    "zh-CN": "安全分析器"
+    "zh-CN": "安全分析器",
+    "ko-KR": "보안 분석기",
+    "no": "Sikkerhetsanalysator",
+    "zh-TW": "安全分析器",
+    "it": "Analizzatore di sicurezza",
+    "pt": "Analisador de segurança",
+    "es": "Analizador de seguridad",
+    "ar": "محلل الأمان",
+    "fr": "Analyseur de sécurité",
+    "tr": "Güvenlik analizörü"
   },
   "CONFIGURATION$SECURITY_SELECT_PLACEHOLDER": {
     "en": "Select a security analyzer (optional)",
     "de": "Wählen Sie einen Sicherheitsanalysator (optional)",
-    "zh-CN": "选择一个安全分析器(可选)"
+    "zh-CN": "选择一个安全分析器(可选)",
+    "ko-KR": "보안 분석기 선택 (선택사항)",
+    "no": "Velg en sikkerhetsanalysator (valgfritt)",
+    "zh-TW": "選擇安全分析器(可選)",
+    "it": "Seleziona un analizzatore di sicurezza (opzionale)",
+    "pt": "Selecione um analisador de segurança (opcional)",
+    "es": "Seleccione un analizador de seguridad (opcional)",
+    "ar": "اختر محلل أمان (اختياري)",
+    "fr": "Sélectionnez un analyseur de sécurité (facultatif)",
+    "tr": "Bir güvenlik analizörü seçin (isteğe bağlı)"
   },
   "CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL": {
     "en": "Close",
@@ -386,100 +428,268 @@
   },
   "CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE": {
     "en": "We've changed some settings in the latest update. Take a minute to review.",
-    "de": "Mit dem letzten Update haben wir ein paar Einstellungen geändert. Bitte kontrollieren Ihre Einstellungen."
+    "de": "Mit dem letzten Update haben wir ein paar Einstellungen geändert. Bitte kontrollieren Ihre Einstellungen.",
+    "zh-CN": "我们在最新更新中更改了一些设置。请花点时间检查一下。",
+    "ko-KR": "최신 업데이트에서 일부 설정을 변경했습니다. 잠시 시간을 내어 검토해 주세요.",
+    "no": "Vi har endret noen innstillinger i den siste oppdateringen. Ta deg tid til å se gjennom dem.",
+    "zh-TW": "我們在最新更新中更改了一些設定。請花點時間檢查一下。",
+    "it": "Abbiamo modificato alcune impostazioni nell'ultimo aggiornamento. Prenditi un momento per rivederle.",
+    "pt": "Alteramos algumas configurações na última atualização. Reserve um momento para revisar.",
+    "es": "Hemos cambiado algunas configuraciones en la última actualización. Tómate un momento para revisarlas.",
+    "ar": "لقد قمنا بتغيير بعض الإعدادات في التحديث الأخير. خذ دقيقة لمراجعتها.",
+    "fr": "Nous avons modifié certains paramètres dans la dernière mise à jour. Prenez un moment pour les examiner.",
+    "tr": "Son güncellemede bazı ayarları değiştirdik. Gözden geçirmek için bir dakikanızı ayırın."
   },
   "CONFIGURATION$AGENT_LOADING": {
-    "en": "Please wait while the agent loads. This may take a few seconds...",
-    "de": "Bitte warten Sie, während der Agent lädt. Das kann ein paar Sekunden dauern..."
+    "en": "Please wait while the agent loads. This may take a few minutes...",
+    "de": "Bitte warten Sie, während der Agent lädt. Das kann ein paar Minuten dauern...",
+    "zh-CN": "请稍候,代理正在加载中。这可能需要几分钟...",
+    "ko-KR": "에이전트가 로드되는 동안 기다려 주세요. 몇 분 정도 걸릴 수 있습니다...",
+    "no": "Vennligst vent mens agenten laster. Dette kan ta noen minutter...",
+    "zh-TW": "請稍候,代理正在載入中。這可能需要幾分鐘...",
+    "it": "Attendere mentre l'agente si carica. Potrebbe richiedere alcuni minuti...",
+    "pt": "Por favor, aguarde enquanto o agente carrega. Isso pode levar alguns minutos...",
+    "es": "Por favor, espere mientras el agente se carga. Esto puede tardar unos minutos...",
+    "ar": "يرجى الانتظار أثناء تحميل الوكيل. قد يستغرق هذا بضع دقائق...",
+    "fr": "Veuillez patienter pendant le chargement de l'agent. Cela peut prendre quelques minutes...",
+    "tr": "Lütfen ajan yüklenirken bekleyin. Bu birkaç dakika sürebilir..."
   },
   "CONFIGURATION$AGENT_RUNNING": {
     "en": "Please stop the agent before editing these settings.",
-    "de": "Bitte beenden Sie den Agenten vor der Bearbeitung der Einstellungen."
+    "de": "Bitte beenden Sie den Agenten vor der Bearbeitung der Einstellungen.",
+    "zh-CN": "请在编辑这些设置之前停止代理。",
+    "ko-KR": "이 설정을 편집하기 전에 에이전트를 중지해 주세요.",
+    "no": "Vennligst stopp agenten før du redigerer disse innstillingene.",
+    "zh-TW": "請在編輯這些設定之前停止代理。",
+    "it": "Si prega di fermare l'agente prima di modificare queste impostazioni.",
+    "pt": "Por favor, pare o agente antes de editar estas configurações.",
+    "es": "Por favor, detenga el agente antes de editar estas configuraciones.",
+    "ar": "يرجى إيقاف الوكيل قبل تعديل هذه الإعدادات.",
+    "fr": "Veuillez arrêter l'agent avant de modifier ces paramètres.",
+    "tr": "Bu ayarları düzenlemeden önce lütfen ajanı durdurun."
   },
   "CONFIGURATION$ERROR_FETCH_MODELS": {
     "en": "Failed to fetch models and agents",
     "zh-CN": "获取模型和智能体失败",
-    "de": "Fehler beim Abrufen der Modelle und Agenten"
+    "de": "Fehler beim Abrufen der Modelle und Agenten",
+    "zh-TW": "獲取模型和智能體失敗",
+    "es": "Error al obtener modelos y agentes",
+    "fr": "Échec de la récupération des modèles et des agents",
+    "it": "Impossibile recuperare modelli e agenti",
+    "pt": "Falha ao buscar modelos e agentes",
+    "ko-KR": "모델 및 에이전트 가져오기 실패",
+    "ar": "فشل في جلب النماذج والوكلاء",
+    "tr": "Modeller ve ajanlar getirilemedi",
+    "no": "Kunne ikke hente modeller og agenter"
   },
   "SESSION$SERVER_CONNECTED_MESSAGE": {
     "en": "Connected to server",
     "zh-CN": "已连接到服务器",
-    "de": "Verbindung zum Server hergestellt"
+    "de": "Verbindung zum Server hergestellt",
+    "zh-TW": "已連接到伺服器",
+    "es": "Conectado al servidor",
+    "fr": "Connecté au serveur",
+    "it": "Connesso al server",
+    "pt": "Conectado ao servidor",
+    "ko-KR": "서버에 연결됨",
+    "ar": "تم الاتصال بالخادم",
+    "tr": "Sunucuya bağlandı",
+    "no": "Koblet til server"
   },
   "SESSION$SESSION_HANDLING_ERROR_MESSAGE": {
     "en": "Error handling message",
     "zh-CN": "处理消息时发生错误",
-    "de": "Fehler beim Verarbeiten der Nachricht"
+    "de": "Fehler beim Verarbeiten der Nachricht",
+    "zh-TW": "處理訊息時發生錯誤",
+    "es": "Error al procesar el mensaje",
+    "fr": "Erreur lors du traitement du message",
+    "it": "Errore durante l'elaborazione del messaggio",
+    "pt": "Erro ao processar a mensagem",
+    "ko-KR": "메시지 처리 중 오류 발생",
+    "ar": "خطأ في معالجة الرسالة",
+    "tr": "Mesaj işlenirken hata oluştu",
+    "no": "Feil ved behandling av melding"
   },
   "SESSION$SESSION_CONNECTION_ERROR_MESSAGE": {
     "en": "Error connecting to session",
     "zh-CN": "连接到会话时发生错误",
-    "de": "Verbindung zur Sitzung fehlgeschlagen"
+    "de": "Verbindung zur Sitzung fehlgeschlagen",
+    "zh-TW": "連接到會話時發生錯誤",
+    "es": "Error al conectar con la sesión",
+    "fr": "Erreur de connexion à la session",
+    "it": "Errore durante la connessione alla sessione",
+    "pt": "Erro ao conectar à sessão",
+    "ko-KR": "세션 연결 오류",
+    "ar": "خطأ في الاتصال بالجلسة",
+    "tr": "Oturuma bağlanırken hata oluştu",
+    "no": "Feil ved tilkobling til økt"
   },
   "SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE": {
     "en": "Socket not initialized",
     "zh-CN": "Socket 未初始化",
-    "de": "Socket nicht initialisiert"
+    "de": "Socket nicht initialisiert",
+    "zh-TW": "Socket 未初始化",
+    "es": "Socket no inicializado",
+    "fr": "Socket non initialisé",
+    "it": "Socket non inizializzato",
+    "pt": "Socket não inicializado",
+    "ko-KR": "소켓이 초기화되지 않았습니다",
+    "ar": "لم يتم تهيئة Socket",
+    "tr": "Soket başlatılmadı"
   },
   "EXPLORER$UPLOAD_ERROR_MESSAGE": {
     "en": "Error uploading file",
     "zh-CN": "上传文件时发生错误",
-    "de": "Fehler beim Hochladen der Datei"
+    "de": "Fehler beim Hochladen der Datei",
+    "zh-TW": "上傳檔案時發生錯誤",
+    "es": "Error al subir el archivo",
+    "fr": "Erreur lors du téléchargement du fichier",
+    "it": "Errore durante il caricamento del file",
+    "pt": "Erro ao fazer upload do arquivo",
+    "ko-KR": "파일 업로드 중 오류 발생",
+    "ar": "خطأ في تحميل الملف",
+    "tr": "Dosya yüklenirken hata oluştu"
   },
   "EXPLORER$LABEL_DROP_FILES": {
     "en": "Drop files here",
     "zh-CN": "将文件拖到这里",
-    "de": "Dateien hier ablegen"
+    "de": "Dateien hier ablegen",
+    "zh-TW": "將檔案拖曳至此",
+    "es": "Suelta los archivos aquí",
+    "fr": "Déposez les fichiers ici",
+    "it": "Trascina i file qui",
+    "pt": "Solte os arquivos aqui",
+    "ko-KR": "파일을 여기에 놓으세요",
+    "ar": "أسقط الملفات هنا",
+    "tr": "Dosyaları buraya bırakın"
   },
   "EXPLORER$LABEL_WORKSPACE": {
     "en": "Workspace",
     "zh-CN": "工作区",
-    "de": "Arbeitsbereich"
+    "de": "Arbeitsbereich",
+    "zh-TW": "工作區",
+    "es": "Espacio de trabajo",
+    "fr": "Espace de travail",
+    "it": "Area di lavoro",
+    "pt": "Espaço de trabalho",
+    "ko-KR": "작업 공간",
+    "ar": "مساحة العمل",
+    "tr": "Çalışma alanı"
   },
   "EXPLORER$EMPTY_WORKSPACE_MESSAGE": {
     "en": "No files in workspace",
     "zh-CN": "工作区没有文件",
-    "de": "Keine Dateien im Arbeitsbereich"
+    "de": "Keine Dateien im Arbeitsbereich",
+    "zh-TW": "工作區沒有檔案",
+    "es": "No hay archivos en el espacio de trabajo",
+    "fr": "Aucun fichier dans l'espace de travail",
+    "it": "Nessun file nell'area di lavoro",
+    "pt": "Nenhum arquivo no espaço de trabalho",
+    "ko-KR": "작업 공간에 파일이 없습니다",
+    "ar": "لا توجد ملفات في مساحة العمل",
+    "tr": "Çalışma alanında dosya yok"
   },
   "EXPLORER$LOADING_WORKSPACE_MESSAGE": {
     "en": "Loading workspace...",
     "zh-CN": "正在加载工作区...",
-    "de": "Arbeitsbereich wird geladen..."
+    "de": "Arbeitsbereich wird geladen...",
+    "zh-TW": "正在載入工作區...",
+    "es": "Cargando espacio de trabajo...",
+    "fr": "Chargement de l'espace de travail...",
+    "it": "Caricamento dell'area di lavoro...",
+    "pt": "Carregando espaço de trabalho...",
+    "ko-KR": "작업 공간 로딩 중...",
+    "ar": "جارٍ تحميل مساحة العمل...",
+    "tr": "Çalışma alanı yükleniyor..."
   },
   "EXPLORER$REFRESH_ERROR_MESSAGE": {
     "en": "Error refreshing workspace",
     "zh-CN": "工作区刷新错误",
-    "de": "Fehler beim Aktualisieren des Arbeitsbereichs"
+    "de": "Fehler beim Aktualisieren des Arbeitsbereichs",
+    "zh-TW": "工作區重新整理錯誤",
+    "es": "Error al actualizar el espacio de trabajo",
+    "fr": "Erreur lors de l'actualisation de l'espace de travail",
+    "it": "Errore durante l'aggiornamento dell'area di lavoro",
+    "pt": "Erro ao atualizar o espaço de trabalho",
+    "ko-KR": "작업 공간 새로 고침 오류",
+    "ar": "خطأ في تحديث مساحة العمل",
+    "tr": "Çalışma alanı yenilenirken hata oluştu"
   },
   "EXPLORER$UPLOAD_SUCCESS_MESSAGE": {
     "en": "Successfully uploaded {{count}} file(s)",
     "zh-CN": "成功上传 {{count}} 个文件",
-    "de": "Erfolgreich {{count}} Datei(en) hochgeladen"
+    "de": "Erfolgreich {{count}} Datei(en) hochgeladen",
+    "zh-TW": "成功上傳 {{count}} 個檔案",
+    "es": "Se subieron {{count}} archivo(s) con éxito",
+    "fr": "{{count}} fichier(s) téléchargé(s) avec succès",
+    "it": "Caricato con successo {{count}} file",
+    "pt": "{{count}} arquivo(s) carregado(s) com sucesso",
+    "ko-KR": "{{count}}개의 파일을 성공적으로 업로드했습니다",
+    "ar": "تم تحميل {{count}} ملف (ملفات) بنجاح",
+    "tr": "{{count}} dosya başarıyla yüklendi"
   },
   "EXPLORER$NO_FILES_UPLOADED_MESSAGE": {
     "en": "No files were uploaded",
     "zh-CN": "没有文件上传",
-    "de": "Keine Dateien wurden hochgeladen"
+    "de": "Keine Dateien wurden hochgeladen",
+    "zh-TW": "沒有檔案被上傳",
+    "es": "No se subieron archivos",
+    "fr": "Aucun fichier n'a été téléchargé",
+    "it": "Nessun file è stato caricato",
+    "pt": "Nenhum arquivo foi carregado",
+    "ko-KR": "업로드된 파일이 없습니다",
+    "ar": "لم يتم تحميل أي ملفات",
+    "tr": "Hiçbir dosya yüklenmedi"
   },
   "EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE": {
     "en": "{{count}} file(s) were skipped during upload",
+    "de": "{{count}} Datei(en) wurden während des Hochladens übersprungen",
     "zh-CN": "{{count}} 个文件在上传过程中被跳过",
-    "de": "{{count}} Datei(en) wurden während des Hochladens übersprungen"
+    "zh-TW": "{{count}} 個檔案在上傳過程中被跳過",
+    "es": "Se omitieron {{count}} archivo(s) durante la carga",
+    "fr": "{{count}} fichier(s) ont été ignorés pendant le téléchargement",
+    "it": "{{count}} file sono stati saltati durante il caricamento",
+    "pt": "{{count}} arquivo(s) foram ignorados durante o upload",
+    "ko-KR": "업로드 중 {{count}}개의 파일이 건너뛰어졌습니다",
+    "ar": "تم تخطي {{count}} ملف (ملفات) أثناء التحميل",
+    "tr": "Yükleme sırasında {{count}} dosya atlandı"
   },
   "EXPLORER$UPLOAD_UNEXPECTED_RESPONSE_MESSAGE": {
     "en": "Unexpected response structure from server",
     "zh-CN": "服务器响应结构不符合预期",
-    "de": "Unerwartetes Antwortformat vom Server"
+    "de": "Unerwartetes Antwortformat vom Server",
+    "zh-TW": "伺服器回應結構不符合預期",
+    "es": "Estructura de respuesta inesperada del servidor",
+    "fr": "Structure de réponse inattendue du serveur",
+    "it": "Struttura di risposta inaspettata dal server",
+    "pt": "Estrutura de resposta inesperada do servidor",
+    "ko-KR": "서버로부터 예상치 못한 응답 구조",
+    "ar": "بنية استجابة غير متوقعة من الخادم",
+    "tr": "Sunucudan beklenmeyen yanıt yapısı"
   },
   "LOAD_SESSION$MODAL_TITLE": {
     "en": "Return to existing session?",
     "de": "Zurück zu vorhandener Sitzung?",
     "zh-CN": "是否继续未完成的会话?",
-    "zh-TW": "是否繼續未完成的會話?"
+    "zh-TW": "是否繼續未完成的會話?",
+    "es": "¿Volver a la sesión existente?",
+    "fr": "Revenir à la session existante ?",
+    "it": "Tornare alla sessione esistente?",
+    "pt": "Retornar à sessão existente?",
+    "ko-KR": "기존 세션으로 돌아가시겠습니까?",
+    "ar": "العودة إلى الجلسة الحالية؟",
+    "tr": "Mevcut oturuma dönmek ister misiniz?"
   },
   "LOAD_SESSION$MODAL_CONTENT": {
     "en": "You seem to have an ongoing session. Would you like to pick up where you left off, or start fresh?",
     "de": "Sie haben eine aktive Sitzung. Möchten Sie die Arbeit an der vorherigen Stelle fortsetzen oder von vorne anfangen?",
+    "es": "Parece que tienes una sesión en curso. ¿Te gustaría continuar donde lo dejaste o empezar de nuevo?",
+    "fr": "Il semble que vous ayez une session en cours. Souhaitez-vous reprendre là où vous vous êtes arrêté ou recommencer à zéro ?",
+    "it": "Sembra che tu abbia una sessione in corso. Vorresti riprendere da dove hai lasciato o ricominciare da capo?",
+    "pt": "Parece que você tem uma sessão em andamento. Gostaria de continuar de onde parou ou começar do zero?",
+    "ko-KR": "진행 중인 세션이 있는 것 같습니다. 중단한 곳에서 계속하시겠습니까, 아니면 새로 시작하시겠습니까?",
+    "ar": "يبدو أن لديك جلسة جارية. هل ترغب في استكمال ما توقفت عنده أم البدء من جديد؟",
+    "tr": "Devam eden bir oturumunuz var gibi görünüyor. Kaldığınız yerden devam etmek mi yoksa yeniden başlamak mı istersiniz?",
     "zh-CN": "您似乎有一个未完成的任务。您想继续之前的工作还是重新开始?",
     "zh-TW": "您似乎有一個未完成的任務。您想從上次離開的地方繼續還是重新開始?"
   },
@@ -487,103 +697,276 @@
     "en": "Resume Session",
     "de": "Sitzung fortsetzen",
     "zh-CN": "恢复会话",
-    "zh-TW": "恢復會話"
+    "zh-TW": "恢復會話",
+    "es": "Reanudar sesión",
+    "fr": "Reprendre la session",
+    "it": "Riprendi sessione",
+    "pt": "Retomar sessão",
+    "ko-KR": "세션 재개",
+    "ar": "استئناف الجلسة",
+    "tr": "Oturumu Devam Ettir"
   },
   "LOAD_SESSION$START_NEW_SESSION_MODAL_ACTION_LABEL": {
     "en": "Start New Session",
     "de": "Neue Sitzung starten",
     "zh-CN": "开始新会话",
-    "zh-TW": "開始新會話"
+    "zh-TW": "開始新會話",
+    "es": "Iniciar nueva sesión",
+    "fr": "Démarrer une nouvelle session",
+    "it": "Avvia nuova sessione",
+    "pt": "Iniciar nova sessão",
+    "ko-KR": "새 세션 시작",
+    "ar": "بدء جلسة جديدة",
+    "tr": "Yeni Oturum Başlat"
   },
   "FEEDBACK$MODAL_TITLE": {
-    "en": "Share feedback"
+    "en": "Share feedback",
+    "de": "Feedback teilen",
+    "zh-CN": "分享反馈",
+    "zh-TW": "分享反饋",
+    "es": "Compartir comentarios",
+    "fr": "Partager des commentaires",
+    "it": "Condividi feedback",
+    "pt": "Compartilhar feedback",
+    "ko-KR": "피드백 공유",
+    "ar": "مشاركة التعليقات",
+    "tr": "Geri bildirim paylaş"
   },
   "FEEDBACK$MODAL_CONTENT": {
-    "en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data."
+    "en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data.",
+    "de": "Um uns zu verbessern, sammeln wir Feedback aus Ihren Interaktionen, um unsere Prompts zu verbessern. Durch das Absenden dieses Formulars stimmen Sie der Erfassung dieser Daten zu.",
+    "zh-CN": "为了帮助我们改进,我们会收集您的互动反馈以改进我们的提示。提交此表单即表示您同意我们收集这些数据。",
+    "zh-TW": "為了幫助我們改進,我們會收集您的互動反饋以改進我們的提示。提交此表單即表示您同意我們收集這些數據。",
+    "es": "Para ayudarnos a mejorar, recopilamos comentarios de sus interacciones para mejorar nuestras indicaciones. Al enviar este formulario, usted consiente que recopilemos estos datos.",
+    "fr": "Pour nous aider à nous améliorer, nous recueillons des commentaires de vos interactions pour améliorer nos invites. En soumettant ce formulaire, vous consentez à ce que nous collections ces données.",
+    "it": "Per aiutarci a migliorare, raccogliamo feedback dalle tue interazioni per migliorare i nostri prompt. Inviando questo modulo, acconsenti alla raccolta di questi dati.",
+    "pt": "Para nos ajudar a melhorar, coletamos feedback de suas interações para aprimorar nossas sugestões. Ao enviar este formulário, você consente que coletemos esses dados.",
+    "ko-KR": "개선을 위해 귀하의 상호 작용에서 피드백을 수집하여 프롬프트를 개선합니다. 이 양식을 제출함으로써 귀하는 이 데이터 수집에 동의하게 됩니다.",
+    "ar": "لمساعدتنا على التحسين، نقوم بجمع التعليقات من تفاعلاتك لتحسين مطالباتنا. من خلال إرسال هذا النموذج، فإنك توافق على جمعنا لهذه البيانات.",
+    "tr": "Kendimizi geliştirmemize yardımcı olmak için, etkileşimlerinizden geri bildirim toplayarak ipuçlarımızı iyileştiriyoruz. Bu formu göndererek, bu verileri toplamamıza izin vermiş olursunuz."
   },
   "FEEDBACK$EMAIL_LABEL": {
-    "en": "Your email"
+    "en": "Your email",
+    "de": "Ihre E-Mail-Adresse",
+    "zh-CN": "您的电子邮箱",
+    "zh-TW": "您的電子郵箱",
+    "es": "Su correo electrónico",
+    "fr": "Votre e-mail",
+    "it": "La tua email",
+    "pt": "Seu e-mail",
+    "ko-KR": "귀하의 이메일",
+    "ar": "بريدك الإلكتروني",
+    "tr": "E-posta adresiniz"
   },
   "FEEDBACK$CONTRIBUTE_LABEL": {
-    "en": "Contribute to public dataset"
+    "en": "Contribute to public dataset",
+    "de": "Zum öffentlichen Datensatz beitragen",
+    "zh-CN": "贡献到公共数据集",
+    "zh-TW": "貢獻到公共數據集",
+    "es": "Contribuir al conjunto de datos público",
+    "fr": "Contribuer à l'ensemble de données public",
+    "it": "Contribuisci al dataset pubblico",
+    "pt": "Contribuir para o conjunto de dados público",
+    "ko-KR": "공개 데이터셋에 기여",
+    "ar": "المساهمة في مجموعة البيانات العامة",
+    "tr": "Genel veri setine katkıda bulun"
   },
   "FEEDBACK$SHARE_LABEL": {
-    "en": "Share"
+    "en": "Share",
+    "de": "Teilen",
+    "zh-CN": "分享",
+    "zh-TW": "分享",
+    "es": "Compartir",
+    "fr": "Partager",
+    "it": "Condividi",
+    "pt": "Compartilhar",
+    "ko-KR": "공유",
+    "ar": "مشاركة",
+    "tr": "Paylaş"
   },
   "FEEDBACK$CANCEL_LABEL": {
-    "en": "Cancel"
+    "en": "Cancel",
+    "de": "Abbruch",
+    "zh-CN": "取消",
+    "zh-TW": "取消",
+    "es": "Cancelar",
+    "fr": "Annuler",
+    "it": "Annulla",
+    "pt": "Cancelar",
+    "ko-KR": "취소",
+    "ar": "إلغاء",
+    "tr": "İptal"
   },
   "FEEDBACK$EMAIL_PLACEHOLDER": {
     "en": "Enter your email address."
   },
   "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": {
-    "en": "Initializing agent (may take up to 10 seconds)...",
-    "zh-CN": "正在初始化智能体(可能需要 10 秒以上时间)",
-    "de": "Agent wird initialisiert (kann bis zu 10 Sekunden dauern)...",
-    "ko-KR": "에이전트 설치중(10초 정도 걸립니다)...",
-    "no": "Initialiserer agent (det kan ta opptil 10 sekunder)...",
-    "zh-TW": "初始化智能體(可能需要 10 秒以上時間)",
-    "it": "Inizializzazione dell'agente (può richiedere fino a 10 secondi)...",
-    "pt": "Inicializando o agente (pode levar até 10 segundos)...",
-    "es": "Inicializando el agente (puede tardar hasta 10 segundos)...",
-    "ar": "جاري تهيئة الوكيل (قد يستغرق حتى 10 ثواني)...",
-    "fr": "Initialisation de l'agent (peut prendre jusqu'à 10 secondes)...",
-    "tr": "Ajan başlatılıyor (bu işlem 10 saniye kadar sürebilir)..."
+    "en": "Starting up!",
+    "de": "Wird gestartet!",
+    "zh-CN": "正在启动!",
+    "zh-TW": "正在啟動!",
+    "ko-KR": "시작 중입니다!",
+    "no": "Starter opp!",
+    "it": "Avvio in corso!",
+    "pt": "Iniciando!",
+    "es": "¡Iniciando!",
+    "ar": "جارٍ البدء!",
+    "fr": "Démarrage en cours !",
+    "tr": "Başlatılıyor!"
   },
   "CHAT_INTERFACE$AGENT_INIT_MESSAGE": {
     "en": "Agent is initialized, waiting for task...",
     "de": "Agent ist initialisiert und wartet auf Aufgabe...",
-    "zh-CN": "智能体已初始化,等待任务中..."
+    "zh-CN": "智能体已初始化,等待任务中...",
+    "zh-TW": "智能體已初始化,等待任務中...",
+    "ko-KR": "에이전트가 초기화되었습니다. 작업을 기다리는 중...",
+    "no": "Agenten er initialisert, venter på oppgave...",
+    "it": "L'agente è inizializzato, in attesa di compiti...",
+    "pt": "Agente inicializado, aguardando tarefa...",
+    "es": "El agente está inicializado, esperando tarea...",
+    "ar": "تم تهيئة الوكيل، في انتظار المهمة...",
+    "fr": "L'agent est initialisé, en attente de tâche...",
+    "tr": "Ajan başlatıldı, görev bekleniyor..."
   },
   "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE": {
     "en": "Agent is running task",
     "de": "Agent führt Aufgabe aus",
-    "zh-CN": "智能体正在执行任务..."
+    "zh-CN": "智能体正在执行任务...",
+    "zh-TW": "智能體正在執行任務...",
+    "ko-KR": "에이전트가 작업을 실행 중입니다",
+    "no": "Agenten utfører oppgave",
+    "it": "L'agente sta eseguendo il compito",
+    "pt": "O agente está executando a tarefa",
+    "es": "El agente está ejecutando la tarea",
+    "ar": "الوكيل يقوم بتنفيذ المهمة",
+    "fr": "L'agent exécute la tâche",
+    "tr": "Ajan görevi yürütüyor"
   },
   "CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE": {
     "en": "Agent is awaiting user input...",
     "de": "Agent wartet auf Benutzereingabe...",
-    "zh-CN": "智能体正在等待用户输入..."
+    "zh-CN": "智能体正在等待用户输入...",
+    "zh-TW": "智能體正在等待用戶輸入...",
+    "ko-KR": "에이전트가 사용자 입력을 기다리고 있습니다...",
+    "no": "Agenten venter på brukerinndata...",
+    "it": "L'agente è in attesa dell'input dell'utente...",
+    "pt": "O agente está aguardando a entrada do usuário...",
+    "es": "El agente está esperando la entrada del usuario...",
+    "ar": "الوكيل في انتظار إدخال المستخدم...",
+    "fr": "L'agent attend l'entrée de l'utilisateur...",
+    "tr": "Ajan kullanıcı girdisini bekliyor..."
   },
   "CHAT_INTERFACE$AGENT_PAUSED_MESSAGE": {
     "en": "Agent has paused.",
     "de": "Agent pausiert.",
-    "zh-CN": "智能体已暂停"
+    "zh-CN": "智能体已暂停",
+    "zh-TW": "智能體已暫停",
+    "ko-KR": "에이전트가 일시 중지되었습니다.",
+    "no": "Agenten har pauset.",
+    "it": "L'agente ha messo in pausa.",
+    "pt": "O agente foi pausado.",
+    "es": "El agente ha pausado.",
+    "ar": "توقف الوكيل مؤقتًا.",
+    "fr": "L'agent a mis en pause.",
+    "tr": "Ajan duraklatıldı."
   },
   "CHAT_INTERFACE$AGENT_STOPPED_MESSAGE": {
     "en": "Agent has stopped.",
     "de": "Agent hat angehalten.",
-    "zh-CN": "智能体已停止"
+    "zh-CN": "智能体已停止",
+    "zh-TW": "智能體已停止",
+    "ko-KR": "에이전트가 중지되었습니다.",
+    "no": "Agenten har stoppet.",
+    "it": "L'agente si è fermato.",
+    "pt": "O agente parou.",
+    "es": "El agente se ha detenido.",
+    "ar": "توقف الوكيل.",
+    "fr": "L'agent s'est arrêté.",
+    "tr": "Ajan durdu."
   },
   "CHAT_INTERFACE$AGENT_FINISHED_MESSAGE": {
     "en": "Agent has finished the task.",
     "de": "Agent hat die Aufgabe erledigt.",
-    "zh-CN": "智能体已完成任务"
+    "zh-CN": "智能体已完成任务",
+    "zh-TW": "智能體已完成任務",
+    "ko-KR": "에이전트가 작업을 완료했습니다.",
+    "no": "Agenten har fullført oppgaven.",
+    "it": "L'agente ha completato il compito.",
+    "pt": "O agente concluiu a tarefa.",
+    "es": "El agente ha terminado la tarea.",
+    "ar": "أنهى الوكيل المهمة.",
+    "fr": "L'agent a terminé la tâche.",
+    "tr": "Ajan görevi tamamladı."
   },
   "CHAT_INTERFACE$AGENT_REJECTED_MESSAGE": {
     "en": "Agent has rejected the task.",
     "de": "Agent hat die Aufgabe abgelehnt.",
-    "zh-CN": "智能体拒绝任务"
+    "zh-CN": "智能体拒绝任务",
+    "zh-TW": "智能體拒絕任務",
+    "ko-KR": "에이전트가 작업을 거부했습니다.",
+    "no": "Agenten har avvist oppgaven.",
+    "it": "L'agente ha rifiutato il compito.",
+    "pt": "O agente rejeitou a tarefa.",
+    "es": "El agente ha rechazado la tarea.",
+    "ar": "رفض الوكيل المهمة.",
+    "fr": "L'agent a rejeté la tâche.",
+    "tr": "Ajan görevi reddetti."
   },
   "CHAT_INTERFACE$AGENT_ERROR_MESSAGE": {
     "en": "Agent encountered an error.",
     "de": "Agent ist auf einen Fehler gelaufen.",
-    "zh-CN": "智能体遇到错误"
+    "zh-CN": "智能体遇到错误",
+    "zh-TW": "智能體遇到錯誤",
+    "ko-KR": "에이전트에 오류가 발생했습니다.",
+    "no": "Agenten støtte på en feil.",
+    "it": "L'agente ha riscontrato un errore.",
+    "pt": "O agente encontrou um erro.",
+    "es": "El agente encontró un error.",
+    "ar": "واجه الوكيل خطأ.",
+    "fr": "L'agent a rencontré une erreur.",
+    "tr": "Ajan bir hatayla karşılaştı."
   },
   "CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE": {
     "en": "Agent is awaiting user confirmation for the pending action.",
     "de": "Agent wartet auf die Bestätigung des Benutzers für die ausstehende Aktion.",
-    "zh-CN": "代理正在等待用户确认待处理的操作。"
+    "zh-CN": "代理正在等待用户确认待处理的操作。",
+    "zh-TW": "代理正在等待用戶確認待處理的操作。",
+    "ko-KR": "에이전트가 대기 중인 작업에 대한 사용자 확인을 기다리고 있습니다.",
+    "no": "Agenten venter på brukerbekreftelse for den ventende handlingen.",
+    "it": "L'agente è in attesa della conferma dell'utente per l'azione in sospeso.",
+    "pt": "O agente está aguardando a confirmação do usuário para a ação pendente.",
+    "es": "El agente está esperando la confirmación del usuario para la acción pendiente.",
+    "ar": "الوكيل ينتظر تأكيد المستخدم للإجراء المعلق.",
+    "fr": "L'agent attend la confirmation de l'utilisateur pour l'action en attente.",
+    "tr": "Ajan, bekleyen işlem için kullanıcı onayını bekliyor."
   },
   "CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE": {
     "en": "Agent action has been confirmed!",
     "de": "Die Aktion des Agenten wurde bestätigt!",
-    "zh-CN": "代理操作已确认!"
+    "zh-CN": "代理操作已确认!",
+    "zh-TW": "代理操作已確認!",
+    "ko-KR": "에이전트 작업이 확인되었습니다!",
+    "no": "Agenthandlingen har blitt bekreftet!",
+    "it": "L'azione dell'agente è stata confermata!",
+    "pt": "A ação do agente foi confirmada!",
+    "es": "¡La acción del agente ha sido confirmada!",
+    "ar": "تم تأكيد إجراء الوكيل!",
+    "fr": "L'action de l'agent a été confirmée !",
+    "tr": "Ajan eylemi onaylandı!"
   },
   "CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE": {
     "en": "Agent action has been rejected!",
     "de": "Die Aktion des Agenten wurde abgelehnt!",
-    "zh-CN": "代理操作已被拒绝!"
+    "zh-CN": "代理操作已被拒绝!",
+    "zh-TW": "代理操作已被拒絕!",
+    "ko-KR": "에이전트 작업이 거부되었습니다!",
+    "no": "Agenthandlingen har blitt avvist!",
+    "it": "L'azione dell'agente è stata rifiutata!",
+    "pt": "A ação do agente foi rejeitada!",
+    "es": "¡La acción del agente ha sido rechazada!",
+    "ar": "تم رفض إجراء الوكيل!",
+    "fr": "L'action de l'agent a été rejetée !",
+    "tr": "Ajan eylemi reddedildi!"
   },
   "CHAT_INTERFACE$INPUT_PLACEHOLDER": {
     "en": "Message assistant...",
@@ -602,22 +985,58 @@
   "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE": {
     "en": "Continue",
     "zh-CN": "继续",
-    "de": "Fortfahren"
+    "de": "Fortfahren",
+    "zh-TW": "繼續",
+    "ko-KR": "계속",
+    "no": "Fortsett",
+    "it": "Continua",
+    "pt": "Continuar",
+    "es": "Continuar",
+    "ar": "استمرار",
+    "fr": "Continuer",
+    "tr": "Devam et"
   },
   "CHAT_INTERFACE$USER_ASK_CONFIRMATION": {
     "en": "Do you want to continue with this action?",
     "de": "Möchten Sie mit dieser Aktion fortfahren?",
-    "zh-CN": "您要继续此操作吗?"
+    "zh-CN": "您要继续此操作吗?",
+    "zh-TW": "您要繼續此操作嗎?",
+    "ko-KR": "이 작업을 계속하시겠습니까?",
+    "no": "Vil du fortsette med denne handlingen?",
+    "it": "Vuoi continuare con questa azione?",
+    "pt": "Deseja continuar com esta ação?",
+    "es": "¿Desea continuar con esta acción?",
+    "ar": "هل تريد الاستمرار في هذا الإجراء؟",
+    "fr": "Voulez-vous continuer avec cette action ?",
+    "tr": "Bu işleme devam etmek istiyor musunuz?"
   },
   "CHAT_INTERFACE$USER_CONFIRMED": {
     "en": "Confirm the requested action",
     "de": "Bestätigen Sie die angeforderte Aktion",
-    "zh-CN": "确认请求的操作"
+    "zh-CN": "确认请求的操作",
+    "zh-TW": "確認請求的操作",
+    "ko-KR": "요청된 작업 확인",
+    "no": "Bekreft den forespurte handlingen",
+    "it": "Conferma l'azione richiesta",
+    "pt": "Confirmar a ação solicitada",
+    "es": "Confirmar la acción solicitada",
+    "ar": "تأكيد الإجراء المطلوب",
+    "fr": "Confirmer l'action demandée",
+    "tr": "İstenen eylemi onayla"
   },
   "CHAT_INTERFACE$USER_REJECTED": {
     "en": "Reject the requested action",
     "de": "Lehnen Sie die angeforderte Aktion ab",
-    "zh-CN": "拒绝请求的操作"
+    "zh-CN": "拒绝请求的操作",
+    "zh-TW": "拒絕請求的操作",
+    "ko-KR": "요청된 작업 거부",
+    "no": "Avvis den forespurte handlingen",
+    "it": "Rifiuta l'azione richiesta",
+    "pt": "Rejeitar a ação solicitada",
+    "es": "Rechazar la acción solicitada",
+    "ar": "رفض الإجراء المطلوب",
+    "fr": "Rejeter l'action demandée",
+    "tr": "İstenen eylemi reddet"
   },
   "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
     "en": "Send",
@@ -635,27 +1054,72 @@
   "CHAT_INTERFACE$CHAT_MESSAGE_COPIED": {
     "en": "Message copied to clipboard",
     "zh-CN": "消息已复制到剪贴板",
-    "de": "Nachricht in die Zwischenablage kopiert"
+    "de": "Nachricht in die Zwischenablage kopiert",
+    "ko-KR": "메시지가 클립보드에 복사되었습니다",
+    "no": "Melding kopiert til utklippstavlen",
+    "zh-TW": "訊息已複製到剪貼簿",
+    "it": "Messaggio copiato negli appunti",
+    "pt": "Mensagem copiada para a área de transferência",
+    "es": "Mensaje copiado al portapapeles",
+    "ar": "تم نسخ الرسالة إلى الحافظة",
+    "fr": "Message copié dans le presse-papiers",
+    "tr": "Mesaj panoya kopyalandı"
   },
   "CHAT_INTERFACE$CHAT_MESSAGE_COPY_FAILED": {
     "en": "Failed to copy message to clipboard",
     "zh-CN": "复制消息到剪贴板失败",
-    "de": "Nachricht konnte nicht in die Zwischenablage kopiert werden"
+    "de": "Nachricht konnte nicht in die Zwischenablage kopiert werden",
+    "ko-KR": "메시지를 클립보드에 복사하지 못했습니다",
+    "no": "Kunne ikke kopiere meldingen til utklippstavlen",
+    "zh-TW": "無法將訊息複製到剪貼簿",
+    "it": "Impossibile copiare il messaggio negli appunti",
+    "pt": "Falha ao copiar mensagem para a área de transferência",
+    "es": "No se pudo copiar el mensaje al portapapeles",
+    "ar": "فشل نسخ الرسالة إلى الحافظة",
+    "fr": "Échec de la copie du message dans le presse-papiers",
+    "tr": "Mesaj panoya kopyalanamadı"
   },
   "CHAT_INTERFACE$TOOLTIP_COPY_MESSAGE": {
     "en": "Copy message",
     "zh-CN": "复制消息",
-    "de": "Nachricht kopieren"
+    "de": "Nachricht kopieren",
+    "ko-KR": "메시지 복사",
+    "no": "Kopier melding",
+    "zh-TW": "複製訊息",
+    "it": "Copia messaggio",
+    "pt": "Copiar mensagem",
+    "es": "Copiar mensaje",
+    "ar": "نسخ الرسالة",
+    "fr": "Copier le message",
+    "tr": "Mesajı kopyala"
   },
   "CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE": {
     "en": "Send message",
     "zh-CN": "发送消息",
-    "de": "Nachricht senden"
+    "de": "Nachricht senden",
+    "ko-KR": "메시지 보내기",
+    "no": "Send melding",
+    "zh-TW": "發送訊息",
+    "it": "Invia messaggio",
+    "pt": "Enviar mensagem",
+    "es": "Enviar mensaje",
+    "ar": "إرسال الرسالة",
+    "fr": "Envoyer le message",
+    "tr": "Mesaj gönder"
   },
   "CHAT_INTERFACE$TOOLTIP_UPLOAD_IMAGE": {
     "en": "Upload image",
     "zh-CN": "上传图片",
-    "de": "Bild hochladen"
+    "de": "Bild hochladen",
+    "ko-KR": "이미지 업로드",
+    "no": "Last opp bilde",
+    "zh-TW": "上傳圖片",
+    "it": "Carica immagine",
+    "pt": "Carregar imagem",
+    "es": "Subir imagen",
+    "ar": "تحميل الصورة",
+    "fr": "Télécharger une image",
+    "tr": "Resim yükle"
   },
   "CHAT_INTERFACE$INITIAL_MESSAGE": {
     "en": "Hi! I'm OpenHands, an AI Software Engineer. What would you like to build with me today?",
@@ -687,7 +1151,16 @@
   "CHAT_INTERFACE$TO_BOTTOM": {
     "en": "To Bottom",
     "de": "Nach unten",
-    "zh-CN": "回到底部"
+    "zh-CN": "回到底部",
+    "ko-KR": "맨 아래로",
+    "no": "Til bunnen",
+    "zh-TW": "回到底部",
+    "it": "In fondo",
+    "pt": "Para o fundo",
+    "es": "Ir al final",
+    "ar": "إلى الأسفل",
+    "fr": "Vers le bas",
+    "tr": "En alta"
   },
   "CHAT_INTERFACE$MESSAGE_ARIA_LABEL": {
     "en": "Message from {{sender}}",
@@ -733,93 +1206,309 @@
   "SECURITY_ANALYZER$UNKNOWN_RISK": {
     "en": "Unknown Risk",
     "de": "Unbekanntes Risiko",
-    "zh-CN": "未知风险"
+    "zh-CN": "未知风险",
+    "ko-KR": "알 수 없는 위험",
+    "no": "Ukjent risiko",
+    "zh-TW": "未知風險",
+    "it": "Rischio sconosciuto",
+    "pt": "Risco desconhecido",
+    "es": "Riesgo desconocido",
+    "ar": "مخاطر غير معروفة",
+    "fr": "Risque inconnu",
+    "tr": "Bilinmeyen risk"
   },
   "SECURITY_ANALYZER$LOW_RISK": {
     "en": "Low Risk",
     "de": "Niedriges Risiko",
-    "zh-CN": "低风险"
+    "zh-CN": "低风险",
+    "ko-KR": "낮은 위험",
+    "no": "Lav risiko",
+    "zh-TW": "低風險",
+    "it": "Rischio basso",
+    "pt": "Baixo risco",
+    "es": "Riesgo bajo",
+    "ar": "مخاطر منخفضة",
+    "fr": "Risque faible",
+    "tr": "Düşük risk"
   },
   "SECURITY_ANALYZER$MEDIUM_RISK": {
     "en": "Medium Risk",
     "de": "Mittleres Risiko",
-    "zh-CN": "中等风险"
+    "zh-CN": "中等风险",
+    "ko-KR": "중간 위험",
+    "no": "Middels risiko",
+    "zh-TW": "中等風險",
+    "it": "Rischio medio",
+    "pt": "Risco médio",
+    "es": "Riesgo medio",
+    "ar": "مخاطر متوسطة",
+    "fr": "Risque moyen",
+    "tr": "Orta risk"
   },
   "SECURITY_ANALYZER$HIGH_RISK": {
     "en": "High Risk",
     "de": "Hohes Risiko",
-    "zh-CN": "高风险"
+    "zh-CN": "高风险",
+    "ko-KR": "높은 위험",
+    "no": "Høy risiko",
+    "zh-TW": "高風險",
+    "it": "Rischio elevato",
+    "pt": "Alto risco",
+    "es": "Riesgo alto",
+    "ar": "مخاطر عالية",
+    "fr": "Risque élevé",
+    "tr": "Yüksek risk"
   },
   "SETTINGS$MODEL_TOOLTIP": {
     "en": "Select the language model to use.",
     "zh-CN": "选择要使用的语言模型",
     "zh-TW": "選擇要使用的語言模型。",
-    "de": "Wähle das zu verwendende Modell."
+    "de": "Wähle das zu verwendende Modell.",
+    "ko-KR": "사용할 언어 모델을 선택하세요.",
+    "no": "Velg språkmodellen som skal brukes.",
+    "it": "Seleziona il modello linguistico da utilizzare.",
+    "pt": "Selecione o modelo de linguagem a ser usado.",
+    "es": "Seleccione el modelo de lenguaje a utilizar.",
+    "ar": "اختر نموذج اللغة المراد استخدامه.",
+    "fr": "Sélectionnez le modèle de langage à utiliser.",
+    "tr": "Kullanılacak dil modelini seçin."
   },
   "SETTINGS$AGENT_TOOLTIP": {
     "en": "Select the agent to use.",
     "zh-CN": "选择要使用的智能体",
     "zh-TW": "選擇要使用的智能體。",
-    "de": "Wähle den zu verwendenden Agenten."
+    "de": "Wähle den zu verwendenden Agenten.",
+    "ko-KR": "사용할 에이전트를 선택하세요.",
+    "no": "Velg agenten som skal brukes.",
+    "it": "Seleziona l'agente da utilizzare.",
+    "pt": "Selecione o agente a ser usado.",
+    "es": "Seleccione el agente a utilizar.",
+    "ar": "اختر الوكيل المراد استخدامه.",
+    "fr": "Sélectionnez l'agent à utiliser.",
+    "tr": "Kullanılacak ajanı seçin."
   },
   "SETTINGS$LANGUAGE_TOOLTIP": {
     "en": "Select the language for the UI.",
     "zh-CN": "选择界面语言",
     "zh-TW": "選擇 UI 的語言。",
-    "de": "Wähle die Sprache für die Oberfläche."
+    "de": "Wähle die Sprache für die Oberfläche.",
+    "ko-KR": "UI 언어를 선택하세요.",
+    "no": "Velg språk for brukergrensesnittet.",
+    "it": "Seleziona la lingua per l'interfaccia utente.",
+    "pt": "Selecione o idioma para a interface do usuário.",
+    "es": "Seleccione el idioma para la interfaz de usuario.",
+    "ar": "اختر لغة واجهة المستخدم.",
+    "fr": "Sélectionnez la langue de l'interface utilisateur.",
+    "tr": "Kullanıcı arayüzü için dil seçin."
   },
   "SETTINGS$DISABLED_RUNNING": {
     "en": "Cannot be changed while the agent is running.",
     "zh-CN": "在智能体运行时无法更改",
     "zh-TW": "智能體正在執行時無法更改。",
-    "de": "Kann bei laufender Aufgabe nicht geändert werden."
+    "de": "Kann bei laufender Aufgabe nicht geändert werden.",
+    "ko-KR": "에이전트가 실행 중일 때는 변경할 수 없습니다.",
+    "no": "Kan ikke endres mens agenten kjører.",
+    "it": "Non può essere modificato mentre l'agente è in esecuzione.",
+    "pt": "Não pode ser alterado enquanto o agente está em execução.",
+    "es": "No se puede cambiar mientras el agente está en ejecución.",
+    "ar": "لا يمكن تغييره أثناء تشغيل الوكيل.",
+    "fr": "Ne peut pas être modifié pendant que l'agent est en cours d'exécution.",
+    "tr": "Ajan çalışırken değiştirilemez."
   },
   "SETTINGS$API_KEY_PLACEHOLDER": {
     "en": "Enter your API key.",
     "zh-CN": "输入您的 API key",
     "zh-TW": "輸入您的 API 金鑰。",
-    "de": "Modell API Schlüssel."
+    "de": "Modell API Schlüssel.",
+    "ko-KR": "API 키를 입력하세요.",
+    "no": "Skriv inn din API-nøkkel.",
+    "it": "Inserisci la tua chiave API.",
+    "pt": "Digite sua chave de API.",
+    "es": "Ingrese su clave de API.",
+    "ar": "أدخل مفتاح API الخاص بك.",
+    "fr": "Entrez votre clé API.",
+    "tr": "API anahtarınızı girin."
   },
   "SETTINGS$CONFIRMATION_MODE": {
     "en": "Enable Confirmation Mode",
     "de": "Bestätigungsmodus aktivieren",
-    "zh-CN": "启用确认模式"
+    "zh-CN": "启用确认模式",
+    "zh-TW": "啟用確認模式",
+    "ko-KR": "확인 모드 활성화",
+    "no": "Aktiver bekreftelsesmodus",
+    "it": "Abilita modalità di conferma",
+    "pt": "Ativar modo de confirmação",
+    "es": "Habilitar modo de confirmación",
+    "ar": "تفعيل وضع التأكيد",
+    "fr": "Activer le mode de confirmation",
+    "tr": "Onay Modunu Etkinleştir"
   },
   "SETTINGS$CONFIRMATION_MODE_TOOLTIP": {
     "en": "Awaits for user confirmation before executing code.",
     "de": "Wartet auf die Bestätigung des Benutzers, bevor der Code ausgeführt wird.",
-    "zh-CN": "在执行代码之前等待用户确认。"
+    "zh-CN": "在执行代码之前等待用户确认。",
+    "zh-TW": "在執行程式碼之前等待使用者確認。",
+    "ko-KR": "코드 실행 전 사용자 확인을 기다립니다.",
+    "no": "Venter på brukerbekreftelse før koden utføres.",
+    "it": "Attende la conferma dell'utente prima di eseguire il codice.",
+    "pt": "Aguarda a confirmação do usuário antes de executar o código.",
+    "es": "Espera la confirmación del usuario antes de ejecutar el código.",
+    "ar": "ينتظر تأكيد المستخدم قبل تنفيذ الكود.",
+    "fr": "Attend la confirmation de l'utilisateur avant d'exécuter le code.",
+    "tr": "Kodu çalıştırmadan önce kullanıcı onayını bekler."
   },
   "SETTINGS$AGENT_SELECT_ENABLED": {
-    "en": "Enable Agent Selection - Advanced Users"
+    "en": "Enable Agent Selection - Advanced Users",
+    "zh-CN": "启用智能体选择 - 高级用户",
+    "zh-TW": "啟用智能體選擇 - 進階使用者",
+    "de": "Agentenauswahl aktivieren - Fortgeschrittene Benutzer",
+    "ko-KR": "에이전트 선택 활성화 - 고급 사용자",
+    "no": "Aktiver agentvalg - Avanserte brukere",
+    "it": "Abilita selezione agente - Utenti avanzati",
+    "pt": "Ativar seleção de agente - Usuários avançados",
+    "es": "Habilitar selección de agente - Usuarios avanzados",
+    "ar": "تمكين اختيار الوكيل - المستخدمين المتقدمين",
+    "fr": "Activer la sélection d'agent - Utilisateurs avancés",
+    "tr": "Ajan Seçimini Etkinleştir - İleri Düzey Kullanıcılar"
   },
   "SETTINGS$SECURITY_ANALYZER": {
     "en": "Enable Security Analyzer",
     "de": "Sicherheitsanalysator aktivieren",
-    "zh-CN": "启用安全分析器"
+    "zh-CN": "启用安全分析器",
+    "zh-TW": "啟用安全分析器",
+    "ko-KR": "보안 분석기 활성화",
+    "no": "Aktiver sikkerhetsanalysator",
+    "it": "Abilita analizzatore di sicurezza",
+    "pt": "Ativar analisador de segurança",
+    "es": "Habilitar analizador de seguridad",
+    "ar": "تمكين محلل الأمان",
+    "fr": "Activer l'analyseur de sécurité",
+    "tr": "Güvenlik Analizörünü Etkinleştir"
   },
   "BROWSER$EMPTY_MESSAGE": {
     "en": "No page loaded.",
     "zh-CN": "页面未加载",
     "zh-TW": "未加載任何頁面。",
-    "de": "Keine Seite geladen."
+    "de": "Keine Seite geladen.",
+    "ko-KR": "페이지가 로드되지 않았습니다.",
+    "no": "Ingen side lastet.",
+    "it": "Nessuna pagina caricata.",
+    "pt": "Nenhuma página carregada.",
+    "es": "Ninguna página cargada.",
+    "ar": "لم يتم تحميل أي صفحة.",
+    "fr": "Aucune page chargée.",
+    "tr": "Sayfa yüklenmedi."
   },
   "PLANNER$EMPTY_MESSAGE": {
     "en": "No plan created.",
     "zh-CN": "计划未创建",
     "zh-TW": "未創建任何計劃。",
-    "de": "Kein Plan erstellt."
+    "de": "Kein Plan erstellt.",
+    "ko-KR": "생성된 계획이 없습니다.",
+    "no": "Ingen plan opprettet.",
+    "it": "Nessun piano creato.",
+    "pt": "Nenhum plano criado.",
+    "es": "Ningún plan creado.",
+    "ar": "لم يتم إنشاء أي خطة.",
+    "fr": "Aucun plan créé.",
+    "tr": "Plan oluşturulmadı."
   },
   "FEEDBACK$PUBLIC_LABEL": {
     "en": "Public",
     "zh-CN": "公开",
-    "zh-TW": "公開。",
-    "de": "Öffentlich"
+    "zh-TW": "公開",
+    "de": "Öffentlich",
+    "ko-KR": "공개",
+    "no": "Offentlig",
+    "it": "Pubblico",
+    "pt": "Público",
+    "es": "Público",
+    "ar": "عام",
+    "fr": "Public",
+    "tr": "Herkese Açık"
   },
   "FEEDBACK$PRIVATE_LABEL": {
     "en": "Private",
     "zh-CN": "私有",
-    "zh-TW": "私有。",
-    "de": "Privat"
+    "zh-TW": "私有",
+    "de": "Privat",
+    "ko-KR": "비공개",
+    "no": "Privat",
+    "it": "Privato",
+    "pt": "Privado",
+    "es": "Privado",
+    "ar": "خاص",
+    "fr": "Privé",
+    "tr": "Özel"
+  },
+  "STATUS$STARTING_RUNTIME": {
+    "en": "Starting Runtime...",
+    "zh-CN": "启动运行时...",
+    "zh-TW": "啟動運行時...",
+    "de": "Laufzeitumgebung wird gestartet...",
+    "ko-KR": "런타임 시작 중...",
+    "no": "Starter kjøretidsmiljø...",
+    "it": "Avvio dell'ambiente di esecuzione...",
+    "pt": "Iniciando o ambiente de execução...",
+    "es": "Iniciando el entorno de ejecución...",
+    "ar": "جارٍ بدء بيئة التشغيل...",
+    "fr": "Démarrage de l'environnement d'exécution...",
+    "tr": "Çalışma zamanı başlatılıyor..."
+  },
+  "STATUS$STARTING_CONTAINER": {
+    "en": "Preparing container, this might take a few minutes...",
+    "zh-CN": "正在准备容器,这可能需要几分钟...",
+    "zh-TW": "正在準備容器,這可能需要幾分鐘...",
+    "de": "Container wird vorbereitet, dies kann einige Minuten dauern...",
+    "ko-KR": "컨테이너를 준비 중입니다. 몇 분 정도 걸릴 수 있습니다...",
+    "no": "Forbereder container, dette kan ta noen minutter...",
+    "it": "Preparazione del container in corso, potrebbe richiedere alcuni minuti...",
+    "pt": "Preparando o container, isso pode levar alguns minutos...",
+    "es": "Preparando el contenedor, esto puede tardar unos minutos...",
+    "ar": "جارٍ إعداد الحاوية، قد يستغرق هذا بضع دقائق...",
+    "fr": "Préparation du conteneur, cela peut prendre quelques minutes...",
+    "tr": "Konteyner hazırlanıyor, bu işlem birkaç dakika sürebilir..."
+  },
+  "STATUS$PREPARING_CONTAINER": {
+    "en": "Preparing to start container...",
+    "zh-CN": "正在准备启动容器...",
+    "zh-TW": "正在準備啟動容器...",
+    "de": "Vorbereitung zum Starten des Containers...",
+    "ko-KR": "컨테이너 시작 준비 중...",
+    "no": "Forbereder å starte container...",
+    "it": "Preparazione all'avvio del container...",
+    "pt": "Preparando para iniciar o container...",
+    "es": "Preparando para iniciar el contenedor...",
+    "ar": "جارٍ التحضير لبدء الحاوية...",
+    "fr": "Préparation du démarrage du conteneur...",
+    "tr": "Konteyner başlatılmaya hazırlanıyor..."
+  },
+  "STATUS$CONTAINER_STARTED": {
+    "en": "Container started.",
+    "zh-CN": "容器已启动。",
+    "zh-TW": "容器已啟動。",
+    "de": "Container gestartet.",
+    "ko-KR": "컨테이너가 시작되었습니다.",
+    "no": "Container startet.",
+    "it": "Container avviato.",
+    "pt": "Container iniciado.",
+    "es": "Contenedor iniciado.",
+    "ar": "تم بدء الحاوية.",
+    "fr": "Conteneur démarré.",
+    "tr": "Konteyner başlatıldı."
+  },
+  "STATUS$WAITING_FOR_CLIENT": {
+    "en": "Waiting for client to become ready...",
+    "zh-CN": "等待客户端准备就绪...",
+    "zh-TW": "等待客戶端準備就緒...",
+    "de": "Warten auf Bereitschaft des Clients...",
+    "ko-KR": "클라이언트가 준비될 때까지 기다리는 중...",
+    "no": "Venter på at klienten skal bli klar...",
+    "it": "In attesa che il client sia pronto...",
+    "pt": "Aguardando o cliente ficar pronto...",
+    "es": "Esperando a que el cliente esté listo...",
+    "ar": "في انتظار جاهزية العميل...",
+    "fr": "En attente que le client soit prêt...",
+    "tr": "İstemcinin hazır olması bekleniyor..."
   }
 }

+ 15 - 2
frontend/src/services/actions.ts

@@ -6,10 +6,11 @@ import {
   ActionSecurityRisk,
   appendSecurityAnalyzerInput,
 } from "#/state/securityAnalyzerSlice";
+import { setCurStatusMessage } from "#/state/statusSlice";
 import { setRootTask } from "#/state/taskSlice";
 import store from "#/store";
 import ActionType from "#/types/ActionType";
-import { ActionMessage } from "#/types/Message";
+import { ActionMessage, StatusMessage } from "#/types/Message";
 import { SocketMessage } from "#/types/ResponseType";
 import { handleObservationMessage } from "./observations";
 import { getRootTask } from "./taskService";
@@ -138,6 +139,16 @@ export function handleActionMessage(message: ActionMessage) {
   }
 }
 
+export function handleStatusMessage(message: StatusMessage) {
+  const msg = message.message == null ? "" : message.message.trim();
+  store.dispatch(
+    setCurStatusMessage({
+      ...message,
+      message: msg,
+    }),
+  );
+}
+
 export function handleAssistantMessage(data: string | SocketMessage) {
   let socketMessage: SocketMessage;
 
@@ -149,7 +160,9 @@ export function handleAssistantMessage(data: string | SocketMessage) {
 
   if ("action" in socketMessage) {
     handleActionMessage(socketMessage);
-  } else {
+  } else if ("observation" in socketMessage) {
     handleObservationMessage(socketMessage);
+  } else if ("message" in socketMessage) {
+    handleStatusMessage(socketMessage);
   }
 }

+ 22 - 2
frontend/src/services/session.ts

@@ -8,11 +8,19 @@ import { I18nKey } from "#/i18n/declaration";
 
 const translate = (key: I18nKey) => i18next.t(key);
 
+// Define a type for the messages
+type Message = {
+  action: ActionType;
+  args: Record<string, unknown>;
+};
+
 class Session {
   private static _socket: WebSocket | null = null;
 
   private static _latest_event_id: number = -1;
 
+  private static _messageQueue: Message[] = [];
+
   public static _history: Record<string, unknown>[] = [];
 
   // callbacks contain a list of callable functions
@@ -83,6 +91,7 @@ class Session {
       toast.success("ws", translate(I18nKey.SESSION$SERVER_CONNECTED_MESSAGE));
       Session._connecting = false;
       Session._initializeAgent();
+      Session._flushQueue();
       Session.callbacks.open?.forEach((callback) => {
         callback(e);
       });
@@ -94,7 +103,6 @@ class Session {
         data = JSON.parse(e.data);
         Session._history.push(data);
       } catch (err) {
-        // TODO: report the error
         toast.error(
           "ws",
           translate(I18nKey.SESSION$SESSION_HANDLING_ERROR_MESSAGE),
@@ -115,6 +123,7 @@ class Session {
     };
 
     Session._socket.onerror = () => {
+      // TODO report error
       toast.error(
         "ws",
         translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE),
@@ -145,9 +154,20 @@ class Session {
     Session._socket = null;
   }
 
+  private static _flushQueue(): void {
+    while (Session._messageQueue.length > 0) {
+      const message = Session._messageQueue.shift();
+      if (message) {
+        setTimeout(() => Session.send(JSON.stringify(message)), 1000);
+      }
+    }
+  }
+
   static send(message: string): void {
+    const messageObject: Message = JSON.parse(message);
+
     if (Session._connecting) {
-      setTimeout(() => Session.send(message), 1000);
+      Session._messageQueue.push(messageObject);
       return;
     }
     if (!Session.isConnected()) {

+ 23 - 0
frontend/src/state/statusSlice.ts

@@ -0,0 +1,23 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { StatusMessage } from "#/types/Message";
+
+const initialStatusMessage: StatusMessage = {
+  message: "",
+  is_error: false,
+};
+
+export const statusSlice = createSlice({
+  name: "status",
+  initialState: {
+    curStatusMessage: initialStatusMessage,
+  },
+  reducers: {
+    setCurStatusMessage: (state, action: PayloadAction<StatusMessage>) => {
+      state.curStatusMessage = action.payload;
+    },
+  },
+});
+
+export const { setCurStatusMessage } = statusSlice.actions;
+
+export default statusSlice.reducer;

+ 2 - 0
frontend/src/store.ts

@@ -8,6 +8,7 @@ import errorsReducer from "./state/errorsSlice";
 import taskReducer from "./state/taskSlice";
 import jupyterReducer from "./state/jupyterSlice";
 import securityAnalyzerReducer from "./state/securityAnalyzerSlice";
+import statusReducer from "./state/statusSlice";
 
 export const rootReducer = combineReducers({
   browser: browserReducer,
@@ -19,6 +20,7 @@ export const rootReducer = combineReducers({
   agent: agentReducer,
   jupyter: jupyterReducer,
   securityAnalyzer: securityAnalyzerReducer,
+  status: statusReducer,
 });
 
 const store = configureStore({

+ 9 - 0
frontend/src/types/Message.tsx

@@ -31,3 +31,12 @@ export interface ObservationMessage {
   // The timestamp of the message
   timestamp: string;
 }
+
+export interface StatusMessage {
+  // TODO not implemented yet
+  // Whether the status is an error, default is false
+  is_error: boolean;
+
+  // A status message to display to the user
+  message: string;
+}

+ 2 - 2
frontend/src/types/ResponseType.tsx

@@ -1,5 +1,5 @@
-import { ActionMessage, ObservationMessage } from "./Message";
+import { ActionMessage, ObservationMessage, StatusMessage } from "./Message";
 
-type SocketMessage = ActionMessage | ObservationMessage;
+type SocketMessage = ActionMessage | ObservationMessage | StatusMessage;
 
 export { type SocketMessage };

+ 0 - 1
openhands/core/main.py

@@ -55,7 +55,6 @@ def create_runtime(
 
     config: The app config.
     sid: The session id.
-    runtime_tools_config: (will be deprecated) The runtime tools config.
     """
     # if sid is provided on the command line, use it as the name of the event stream
     # otherwise generate it on the basis of the configured jwt_secret

+ 31 - 0
openhands/runtime/client/client.py

@@ -16,8 +16,10 @@ from pathlib import Path
 
 import pexpect
 from fastapi import FastAPI, HTTPException, Request, UploadFile
+from fastapi.exceptions import RequestValidationError
 from fastapi.responses import JSONResponse
 from pydantic import BaseModel
+from starlette.exceptions import HTTPException as StarletteHTTPException
 from uvicorn import run
 
 from openhands.core.logger import openhands_logger as logger
@@ -562,6 +564,35 @@ if __name__ == '__main__':
 
     app = FastAPI(lifespan=lifespan)
 
+    # TODO below 3 exception handlers were recommended by Sonnet.
+    # Are these something we should keep?
+    @app.exception_handler(Exception)
+    async def global_exception_handler(request: Request, exc: Exception):
+        logger.exception('Unhandled exception occurred:')
+        return JSONResponse(
+            status_code=500,
+            content={
+                'message': 'An unexpected error occurred. Please try again later.'
+            },
+        )
+
+    @app.exception_handler(StarletteHTTPException)
+    async def http_exception_handler(request: Request, exc: StarletteHTTPException):
+        logger.error(f'HTTP exception occurred: {exc.detail}')
+        return JSONResponse(
+            status_code=exc.status_code, content={'message': exc.detail}
+        )
+
+    @app.exception_handler(RequestValidationError)
+    async def validation_exception_handler(
+        request: Request, exc: RequestValidationError
+    ):
+        logger.error(f'Validation error occurred: {exc}')
+        return JSONResponse(
+            status_code=422,
+            content={'message': 'Invalid request parameters', 'details': exc.errors()},
+        )
+
     @app.middleware('http')
     async def one_request_at_a_time(request: Request, call_next):
         assert client is not None

+ 27 - 10
openhands/runtime/client/runtime.py

@@ -2,6 +2,7 @@ import os
 import tempfile
 import threading
 import uuid
+from typing import Callable
 from zipfile import ZipFile
 
 import docker
@@ -119,6 +120,7 @@ class EventStreamRuntime(Runtime):
         sid: str = 'default',
         plugins: list[PluginRequirement] | None = None,
         env_vars: dict[str, str] | None = None,
+        status_message_callback: Callable | None = None,
     ):
         self.config = config
         self._host_port = 30000  # initial dummy value
@@ -130,12 +132,13 @@ class EventStreamRuntime(Runtime):
         self.instance_id = (
             sid + '_' + str(uuid.uuid4()) if sid is not None else str(uuid.uuid4())
         )
+        self.status_message_callback = status_message_callback
 
+        self.send_status_message('STATUS$STARTING_RUNTIME')
         self.docker_client: docker.DockerClient = self._init_docker_client()
         self.base_container_image = self.config.sandbox.base_container_image
         self.runtime_container_image = self.config.sandbox.runtime_container_image
         self.container_name = self.container_name_prefix + self.instance_id
-
         self.container = None
         self.action_semaphore = threading.Semaphore(1)  # Ensure one action at a time
 
@@ -146,9 +149,10 @@ class EventStreamRuntime(Runtime):
         self.log_buffer: LogBuffer | None = None
 
         if self.config.sandbox.runtime_extra_deps:
-            logger.info(
+            logger.debug(
                 f'Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.runtime_extra_deps}'
             )
+
         self.skip_container_logs = (
             os.environ.get('SKIP_CONTAINER_LOGS', 'false').lower() == 'true'
         )
@@ -157,6 +161,8 @@ class EventStreamRuntime(Runtime):
                 raise ValueError(
                     'Neither runtime container image nor base container image is set'
                 )
+            logger.info('Preparing container, this might take a few minutes...')
+            self.send_status_message('STATUS$STARTING_CONTAINER')
             self.runtime_container_image = build_runtime_image(
                 self.base_container_image,
                 self.runtime_builder,
@@ -169,9 +175,13 @@ class EventStreamRuntime(Runtime):
         )
 
         # will initialize both the event stream and the env vars
-        super().__init__(config, event_stream, sid, plugins, env_vars)
+        super().__init__(
+            config, event_stream, sid, plugins, env_vars, status_message_callback
+        )
+
+        logger.info('Waiting for client to become ready...')
+        self.send_status_message('STATUS$WAITING_FOR_CLIENT')
 
-        logger.info('Waiting for runtime container to be alive...')
         self._wait_until_alive()
 
         self.setup_initial_env()
@@ -179,6 +189,7 @@ class EventStreamRuntime(Runtime):
         logger.info(
             f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}'
         )
+        self.send_status_message(' ')
 
     @staticmethod
     def _init_docker_client() -> docker.DockerClient:
@@ -201,9 +212,8 @@ class EventStreamRuntime(Runtime):
         plugins: list[PluginRequirement] | None = None,
     ):
         try:
-            logger.info(
-                f'Starting container with image: {self.runtime_container_image} and name: {self.container_name}'
-            )
+            logger.info('Preparing to start container...')
+            self.send_status_message('STATUS$PREPARING_CONTAINER')
             plugin_arg = ''
             if plugins is not None and len(plugins) > 0:
                 plugin_arg = (
@@ -241,17 +251,17 @@ class EventStreamRuntime(Runtime):
             if self.config.debug:
                 environment['DEBUG'] = 'true'
 
-            logger.info(f'Workspace Base: {self.config.workspace_base}')
+            logger.debug(f'Workspace Base: {self.config.workspace_base}')
             if mount_dir is not None and sandbox_workspace_dir is not None:
                 # e.g. result would be: {"/home/user/openhands/workspace": {'bind': "/workspace", 'mode': 'rw'}}
                 volumes = {mount_dir: {'bind': sandbox_workspace_dir, 'mode': 'rw'}}
-                logger.info(f'Mount dir: {mount_dir}')
+                logger.debug(f'Mount dir: {mount_dir}')
             else:
                 logger.warn(
                     'Warning: Mount dir is not set, will not mount the workspace directory to the container!\n'
                 )
                 volumes = None
-            logger.info(f'Sandbox workspace: {sandbox_workspace_dir}')
+            logger.debug(f'Sandbox workspace: {sandbox_workspace_dir}')
 
             if self.config.sandbox.browsergym_eval_env is not None:
                 browsergym_arg = (
@@ -259,6 +269,7 @@ class EventStreamRuntime(Runtime):
                 )
             else:
                 browsergym_arg = ''
+
             container = self.docker_client.containers.run(
                 self.runtime_container_image,
                 command=(
@@ -281,6 +292,7 @@ class EventStreamRuntime(Runtime):
             )
             self.log_buffer = LogBuffer(container)
             logger.info(f'Container started. Server url: {self.api_url}')
+            self.send_status_message('STATUS$CONTAINER_STARTED')
             return container
         except Exception as e:
             logger.error(
@@ -539,3 +551,8 @@ class EventStreamRuntime(Runtime):
                 return port
         # If no port is found after max_attempts, return the last tried port
         return port
+
+    def send_status_message(self, message: str):
+        """Sends a status message if the callback function was provided."""
+        if self.status_message_callback:
+            self.status_message_callback(message)

+ 10 - 1
openhands/runtime/e2b/runtime.py

@@ -1,3 +1,5 @@
+from typing import Callable, Optional
+
 from openhands.core.config import AppConfig
 from openhands.events.action import (
     FileReadAction,
@@ -25,8 +27,15 @@ class E2BRuntime(Runtime):
         sid: str = 'default',
         plugins: list[PluginRequirement] | None = None,
         sandbox: E2BSandbox | None = None,
+        status_message_callback: Optional[Callable] = None,
     ):
-        super().__init__(config, event_stream, sid, plugins)
+        super().__init__(
+            config,
+            event_stream,
+            sid,
+            plugins,
+            status_message_callback=status_message_callback,
+        )
         if sandbox is None:
             self.sandbox = E2BSandbox()
         if not isinstance(self.sandbox, E2BSandbox):

+ 5 - 1
openhands/runtime/remote/runtime.py

@@ -2,6 +2,7 @@ import os
 import tempfile
 import threading
 import uuid
+from typing import Callable, Optional
 from zipfile import ZipFile
 
 import requests
@@ -55,6 +56,7 @@ class RemoteRuntime(Runtime):
         sid: str = 'default',
         plugins: list[PluginRequirement] | None = None,
         env_vars: dict[str, str] | None = None,
+        status_message_callback: Optional[Callable] = None,
     ):
         self.config = config
         if self.config.sandbox.api_hostname == 'localhost':
@@ -168,7 +170,9 @@ class RemoteRuntime(Runtime):
         )
 
         # Initialize the eventstream and env vars
-        super().__init__(config, event_stream, sid, plugins, env_vars)
+        super().__init__(
+            config, event_stream, sid, plugins, env_vars, status_message_callback
+        )
 
         logger.info(
             f'Runtime initialized with plugins: {[plugin.name for plugin in self.plugins]}'

+ 3 - 0
openhands/runtime/runtime.py

@@ -3,6 +3,7 @@ import copy
 import json
 import os
 from abc import abstractmethod
+from typing import Callable
 
 from openhands.core.config import AppConfig, SandboxConfig
 from openhands.core.logger import openhands_logger as logger
@@ -58,11 +59,13 @@ class Runtime:
         sid: str = 'default',
         plugins: list[PluginRequirement] | None = None,
         env_vars: dict[str, str] | None = None,
+        status_message_callback: Callable | None = None,
     ):
         self.sid = sid
         self.event_stream = event_stream
         self.event_stream.subscribe(EventStreamSubscriber.RUNTIME, self.on_event)
         self.plugins = plugins if plugins is not None and len(plugins) > 0 else []
+        self.status_message_callback = status_message_callback
 
         self.config = copy.deepcopy(config)
         atexit.register(self.close)

+ 26 - 8
openhands/server/session/agent.py → openhands/server/session/agent_session.py

@@ -1,3 +1,6 @@
+import asyncio
+from typing import Callable, Optional
+
 from openhands.controller import AgentController
 from openhands.controller.agent import Agent
 from openhands.controller.state.state import State
@@ -46,9 +49,9 @@ class AgentSession:
         max_budget_per_task: float | None = None,
         agent_to_llm_config: dict[str, LLMConfig] | None = None,
         agent_configs: dict[str, AgentConfig] | None = None,
+        status_message_callback: Optional[Callable] = None,
     ):
         """Starts the Agent session
-
         Parameters:
         - runtime_name: The name of the runtime associated with the session
         - config:
@@ -58,13 +61,12 @@ class AgentSession:
         - agent_to_llm_config:
         - agent_configs:
         """
-
         if self.controller or self.runtime:
             raise RuntimeError(
                 'Session already started. You need to close this session and start a new one.'
             )
         await self._create_security_analyzer(config.security.security_analyzer)
-        await self._create_runtime(runtime_name, config, agent)
+        await self._create_runtime(runtime_name, config, agent, status_message_callback)
         await self._create_controller(
             agent,
             config.security.confirmation_mode,
@@ -96,13 +98,19 @@ class AgentSession:
         - security_analyzer: The name of the security analyzer to use
         """
 
-        logger.info(f'Using security analyzer: {security_analyzer}')
         if security_analyzer:
+            logger.debug(f'Using security analyzer: {security_analyzer}')
             self.security_analyzer = options.SecurityAnalyzers.get(
                 security_analyzer, SecurityAnalyzer
             )(self.event_stream)
 
-    async def _create_runtime(self, runtime_name: str, config: AppConfig, agent: Agent):
+    async def _create_runtime(
+        self,
+        runtime_name: str,
+        config: AppConfig,
+        agent: Agent,
+        status_message_callback: Optional[Callable] = None,
+    ):
         """Creates a runtime instance
 
         Parameters:
@@ -112,17 +120,27 @@ class AgentSession:
         """
 
         if self.runtime is not None:
-            raise Exception('Runtime already created')
+            raise RuntimeError('Runtime already created')
 
         logger.info(f'Initializing runtime `{runtime_name}` now...')
         runtime_cls = get_runtime_cls(runtime_name)
-        self.runtime = runtime_cls(
+
+        self.runtime = await asyncio.to_thread(
+            runtime_cls,
             config=config,
             event_stream=self.event_stream,
             sid=self.sid,
             plugins=agent.sandbox_plugins,
+            status_message_callback=status_message_callback,
         )
 
+        if self.runtime is not None:
+            logger.debug(
+                f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}'
+            )
+        else:
+            logger.warning('Runtime initialization failed')
+
     async def _create_controller(
         self,
         agent: Agent,
@@ -178,5 +196,5 @@ class AgentSession:
             )
             logger.info(f'Restored agent state from session, sid: {self.sid}')
         except Exception as e:
-            logger.info(f'Error restoring state: {e}')
+            logger.info(f'State could not be restored: {e}')
         logger.info('Agent controller initialized.')

+ 4 - 2
openhands/server/session/manager.py

@@ -35,9 +35,11 @@ class SessionManager:
 
     async def send(self, sid: str, data: dict[str, object]) -> bool:
         """Sends data to the client."""
-        if sid not in self._sessions:
+        session = self.get_session(sid)
+        if session is None:
+            logger.error(f'*** No session found for {sid}, skipping message ***')
             return False
-        return await self._sessions[sid].send(data)
+        return await session.send(data)
 
     async def send_error(self, sid: str, message: str) -> bool:
         """Sends an error message to the client."""

+ 14 - 3
openhands/server/session/session.py

@@ -21,7 +21,7 @@ from openhands.events.serialization import event_from_dict, event_to_dict
 from openhands.events.stream import EventStreamSubscriber
 from openhands.llm.llm import LLM
 from openhands.runtime.utils.shutdown_listener import should_continue
-from openhands.server.session.agent import AgentSession
+from openhands.server.session.agent_session import AgentSession
 from openhands.storage.files import FileStore
 
 DEL_DELT_SEC = 60 * 60 * 5
@@ -33,6 +33,7 @@ class Session:
     last_active_ts: int = 0
     is_alive: bool = True
     agent_session: AgentSession
+    loop: asyncio.AbstractEventLoop
 
     def __init__(
         self, sid: str, ws: WebSocket | None, config: AppConfig, file_store: FileStore
@@ -45,6 +46,7 @@ class Session:
             EventStreamSubscriber.SERVER, self.on_event
         )
         self.config = config
+        self.loop = asyncio.get_event_loop()
 
     async def close(self):
         self.is_alive = False
@@ -113,6 +115,7 @@ class Session:
                 max_budget_per_task=self.config.max_budget_per_task,
                 agent_to_llm_config=self.config.get_agent_to_llm_config_map(),
                 agent_configs=self.config.get_agent_configs(),
+                status_message_callback=self.queue_status_message,
             )
         except Exception as e:
             logger.exception(f'Error creating controller: {e}')
@@ -125,7 +128,8 @@ class Session:
         )
 
     async def on_event(self, event: Event):
-        """Callback function for agent events.
+        """Callback function for events that mainly come from the agent.
+        Event is the base class for any agent action and observation.
 
         Args:
             event: The agent event (Observation or Action).
@@ -135,7 +139,6 @@ class Session:
         if isinstance(event, NullObservation):
             return
         if event.source == EventSource.AGENT:
-            logger.info('Server event')
             await self.send(event_to_dict(event))
         elif event.source == EventSource.USER and isinstance(
             event, CmdOutputObservation
@@ -172,6 +175,9 @@ class Session:
             await asyncio.sleep(0.001)  # This flushes the data to the client
             self.last_active_ts = int(time.time())
             return True
+        except RuntimeError:
+            self.is_alive = False
+            return False
         except WebSocketDisconnect:
             self.is_alive = False
             return False
@@ -195,3 +201,8 @@ class Session:
             return False
         self.is_alive = data.get('is_alive', False)
         return True
+
+    def queue_status_message(self, message: str):
+        """Queues a status message to be sent asynchronously."""
+        # Ensure the coroutine runs in the main event loop
+        asyncio.run_coroutine_threadsafe(self.send_message(message), self.loop)