+ )}
{totalEvents > 0 && !isV1Conversation && (
diff --git a/frontend/src/components/features/chat/chat-status-indicator.tsx b/frontend/src/components/features/chat/chat-status-indicator.tsx
new file mode 100644
index 0000000000..de23b980fe
--- /dev/null
+++ b/frontend/src/components/features/chat/chat-status-indicator.tsx
@@ -0,0 +1,53 @@
+import { cn } from "@heroui/react";
+import { motion, AnimatePresence } from "framer-motion";
+import DebugStackframeDot from "#/icons/debug-stackframe-dot.svg?react";
+
+interface ChatStatusIndicatorProps {
+ status: string;
+ statusColor: string;
+}
+
+function ChatStatusIndicator({
+ status,
+ statusColor,
+}: ChatStatusIndicatorProps) {
+ return (
+
+
+ {/* Dot */}
+
+
+
+
+ {/* Text */}
+
+ {status}
+
+
+
+ );
+}
+
+export default ChatStatusIndicator;
diff --git a/frontend/src/components/features/controls/server-status.tsx b/frontend/src/components/features/controls/server-status.tsx
index e79d4215ea..eec5ed565b 100644
--- a/frontend/src/components/features/controls/server-status.tsx
+++ b/frontend/src/components/features/controls/server-status.tsx
@@ -1,11 +1,10 @@
import { useTranslation } from "react-i18next";
import DebugStackframeDot from "#/icons/debug-stackframe-dot.svg?react";
-import { I18nKey } from "#/i18n/declaration";
import { ConversationStatus } from "#/types/conversation-status";
import { AgentState } from "#/types/agent-state";
import { useAgentState } from "#/hooks/use-agent-state";
import { useTaskPolling } from "#/hooks/query/use-task-polling";
-import { getStatusColor } from "#/utils/utils";
+import { getStatusColor, getStatusText } from "#/utils/utils";
import { useErrorMessageStore } from "#/stores/error-message-store";
export interface ServerStatusProps {
@@ -20,13 +19,12 @@ export function ServerStatus({
isPausing = false,
}: ServerStatusProps) {
const { curAgentState } = useAgentState();
- const { t } = useTranslation();
const { isTask, taskStatus, taskDetail } = useTaskPolling();
+ const { t } = useTranslation();
const { errorMessage } = useErrorMessageStore();
const isStartingStatus =
curAgentState === AgentState.LOADING || curAgentState === AgentState.INIT;
-
const isStopStatus = conversationStatus === "STOPPED";
const statusColor = getStatusColor({
@@ -38,45 +36,17 @@ export function ServerStatus({
curAgentState,
});
- const getStatusText = (): string => {
- // Show pausing status
- if (isPausing) {
- return t(I18nKey.COMMON$STOPPING);
- }
-
- // Show task status if we're polling a task
- if (isTask && taskStatus) {
- if (taskStatus === "ERROR") {
- return (
- taskDetail || t(I18nKey.CONVERSATION$ERROR_STARTING_CONVERSATION)
- );
- }
- if (taskStatus === "READY") {
- return t(I18nKey.CONVERSATION$READY);
- }
- // Format status text: "WAITING_FOR_SANDBOX" -> "Waiting for sandbox"
- return (
- taskDetail ||
- taskStatus
- .toLowerCase()
- .replace(/_/g, " ")
- .replace(/\b\w/g, (c) => c.toUpperCase())
- );
- }
-
- if (isStartingStatus) {
- return t(I18nKey.COMMON$STARTING);
- }
- if (isStopStatus) {
- return t(I18nKey.COMMON$SERVER_STOPPED);
- }
- if (curAgentState === AgentState.ERROR) {
- return errorMessage || t(I18nKey.COMMON$ERROR);
- }
- return t(I18nKey.COMMON$RUNNING);
- };
-
- const statusText = getStatusText();
+ const statusText = getStatusText({
+ isPausing,
+ isTask,
+ taskStatus,
+ taskDetail,
+ isStartingStatus,
+ isStopStatus,
+ curAgentState,
+ errorMessage,
+ t,
+ });
return (
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts
index 30645cb4f7..4499695392 100644
--- a/frontend/src/i18n/declaration.ts
+++ b/frontend/src/i18n/declaration.ts
@@ -929,6 +929,7 @@ export enum I18nKey {
COMMON$RECENT_PROJECTS = "COMMON$RECENT_PROJECTS",
COMMON$RUN = "COMMON$RUN",
COMMON$RUNNING = "COMMON$RUNNING",
+ COMMON$WAITING_FOR_SANDBOX = "COMMON$WAITING_FOR_SANDBOX",
COMMON$SELECT_GIT_PROVIDER = "COMMON$SELECT_GIT_PROVIDER",
COMMON$SERVER_STATUS = "COMMON$SERVER_STATUS",
COMMON$SERVER_STOPPED = "COMMON$SERVER_STOPPED",
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index 5c1f36af07..a602b90777 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -14863,6 +14863,22 @@
"de": "Läuft",
"uk": "Працює"
},
+ "COMMON$WAITING_FOR_SANDBOX": {
+ "en": "Waiting for sandbox",
+ "ja": "サンドボックスを待機中",
+ "zh-CN": "等待沙盒",
+ "zh-TW": "等待沙盒",
+ "ko-KR": "샌드박스를 기다리는 중",
+ "no": "Venter på sandkasse",
+ "it": "In attesa del sandbox",
+ "pt": "Aguardando sandbox",
+ "es": "Esperando el sandbox",
+ "ar": "في انتظار البيئة المعزولة",
+ "fr": "En attente du bac à sable",
+ "tr": "Sandbox bekleniyor",
+ "de": "Warten auf Sandbox",
+ "uk": "Очікування пісочниці"
+ },
"COMMON$SELECT_GIT_PROVIDER": {
"en": "Select Git provider",
"ja": "Gitプロバイダーを選択",
diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts
index c3d6a900c4..3c7e58f398 100644
--- a/frontend/src/utils/utils.ts
+++ b/frontend/src/utils/utils.ts
@@ -7,6 +7,7 @@ import { GitRepository } from "#/types/git";
import { sanitizeQuery } from "#/utils/sanitize-query";
import { PRODUCT_URL } from "#/utils/constants";
import { AgentState } from "#/types/agent-state";
+import { I18nKey } from "#/i18n/declaration";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -746,3 +747,91 @@ export const getStatusColor = (options: {
}
return "#BCFF8C";
};
+
+interface GetStatusTextArgs {
+ isPausing: boolean;
+ isTask: boolean;
+ taskStatus?: string | null;
+ taskDetail?: string | null;
+ isStartingStatus: boolean;
+ isStopStatus: boolean;
+ curAgentState: AgentState;
+ errorMessage?: string | null;
+ t: (t: string) => string;
+}
+
+/**
+ * Get the server status text based on agent and task state
+ *
+ * @param options Configuration object for status text calculation
+ * @param options.isPausing Whether the agent is currently pausing
+ * @param options.isTask Whether we're polling a task
+ * @param options.taskStatus The task status string (e.g., "ERROR", "READY")
+ * @param options.taskDetail Optional task-specific detail text
+ * @param options.isStartingStatus Whether the conversation is in STARTING state
+ * @param options.isStopStatus Whether the conversation is STOPPED
+ * @param options.curAgentState The current agent state
+ * @param options.errorMessage Optional agent error message
+ * @returns Localized human-readable status text
+ *
+ * @example
+ * getStatusText({
+ * isPausing: false,
+ * isTask: true,
+ * taskStatus: "WAITING_FOR_SANDBOX",
+ * taskDetail: null,
+ * isStartingStatus: false,
+ * isStopStatus: false,
+ * curAgentState: AgentState.RUNNING
+ * }) // Returns "Waiting For Sandbox"
+ */
+export function getStatusText({
+ isPausing = false,
+ isTask,
+ taskStatus,
+ taskDetail,
+ isStartingStatus,
+ isStopStatus,
+ curAgentState,
+ errorMessage,
+ t,
+}: GetStatusTextArgs): string {
+ // Show pausing status
+ if (isPausing) {
+ return t(I18nKey.COMMON$STOPPING);
+ }
+
+ // Show task status if we're polling a task
+ if (isTask && taskStatus) {
+ if (taskStatus === "ERROR") {
+ return taskDetail || t(I18nKey.CONVERSATION$ERROR_STARTING_CONVERSATION);
+ }
+
+ if (taskStatus === "READY") {
+ return t(I18nKey.CONVERSATION$READY);
+ }
+
+ // Format status text: "WAITING_FOR_SANDBOX" -> "Waiting for sandbox"
+ return (
+ taskDetail ||
+ taskStatus
+ .toLowerCase()
+ .replace(/_/g, " ")
+ .replace(/\b\w/g, (c) => c.toUpperCase())
+ );
+ }
+
+ if (isStartingStatus) {
+ return t(I18nKey.COMMON$STARTING);
+ }
+
+ if (isStopStatus) {
+ return t(I18nKey.COMMON$SERVER_STOPPED);
+ }
+
+ if (curAgentState === AgentState.ERROR) {
+ return errorMessage || t(I18nKey.COMMON$ERROR);
+ }
+
+ return t(I18nKey.COMMON$RUNNING);
+}