diff --git a/frontend/__tests__/context/ws-client-provider.test.tsx b/frontend/__tests__/context/ws-client-provider.test.tsx index c27d9b38fa..00de1947ba 100644 --- a/frontend/__tests__/context/ws-client-provider.test.tsx +++ b/frontend/__tests__/context/ws-client-provider.test.tsx @@ -56,15 +56,15 @@ function TestComponent() { describe("WsClientProvider", () => { beforeEach(() => { vi.clearAllMocks(); - vi.mock("#/hooks/query/use-user-conversation", () => ({ - useUserConversation: () => { + vi.mock("#/hooks/query/use-active-conversation", () => ({ + useActiveConversation: () => { return { data: { conversation_id: "1", title: "Conversation 1", selected_repository: null, last_updated_at: "2021-10-01T12:00:00Z", created_at: "2021-10-01T12:00:00Z", - status: "STOPPED" as const, + status: "RUNNING" as const, url: null, session_api_key: null, }}}, diff --git a/frontend/src/components/features/chat/action-suggestions.tsx b/frontend/src/components/features/chat/action-suggestions.tsx index e07b5b07e0..4bf65c9f95 100644 --- a/frontend/src/components/features/chat/action-suggestions.tsx +++ b/frontend/src/components/features/chat/action-suggestions.tsx @@ -4,8 +4,7 @@ import { useTranslation } from "react-i18next"; import { SuggestionItem } from "#/components/features/suggestions/suggestion-item"; import { I18nKey } from "#/i18n/declaration"; import { useUserProviders } from "#/hooks/use-user-providers"; -import { useConversationId } from "#/hooks/use-conversation-id"; -import { useUserConversation } from "#/hooks/query/use-user-conversation"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; interface ActionSuggestionsProps { onSuggestionsClick: (value: string) => void; @@ -16,9 +15,7 @@ export function ActionSuggestions({ }: ActionSuggestionsProps) { const { t } = useTranslation(); const { providers } = useUserProviders(); - const { conversationId } = useConversationId(); - const { data: conversation } = useUserConversation(conversationId); - + const { data: conversation } = useActiveConversation(); const [hasPullRequest, setHasPullRequest] = React.useState(false); const providersAreSet = providers.length > 0; diff --git a/frontend/src/components/features/controls/agent-status-bar.tsx b/frontend/src/components/features/controls/agent-status-bar.tsx index 17513e610a..efa9be15b7 100644 --- a/frontend/src/components/features/controls/agent-status-bar.tsx +++ b/frontend/src/components/features/controls/agent-status-bar.tsx @@ -15,6 +15,7 @@ import { } from "#/context/ws-client-provider"; import { useNotification } from "#/hooks/useNotification"; import { browserTab } from "#/utils/browser-tab"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; const notificationStates = [ AgentState.AWAITING_USER_INPUT, @@ -28,6 +29,7 @@ export function AgentStatusBar() { const { curStatusMessage } = useSelector((state: RootState) => state.status); const { status } = useWsClient(); const { notify } = useNotification(); + const { data: conversation } = useActiveConversation(); const [statusMessage, setStatusMessage] = React.useState(""); @@ -78,7 +80,10 @@ export function AgentStatusBar() { ); React.useEffect(() => { - if (status === WsClientProviderStatus.DISCONNECTED) { + if (conversation?.status === "STARTING") { + setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME)); + setIndicatorColor(IndicatorColor.RED); + } else if (status === WsClientProviderStatus.DISCONNECTED) { setStatusMessage(t(I18nKey.STATUS$CONNECTED)); // Using STATUS$CONNECTED instead of STATUS$CONNECTING setIndicatorColor(IndicatorColor.RED); } else { @@ -97,7 +102,7 @@ export function AgentStatusBar() { } } } - }, [curAgentState, status, notify, t]); + }, [curAgentState, status, notify, t, conversation?.status]); return (
diff --git a/frontend/src/components/features/controls/controls.tsx b/frontend/src/components/features/controls/controls.tsx index 6edf6d6490..3afe4cdf30 100644 --- a/frontend/src/components/features/controls/controls.tsx +++ b/frontend/src/components/features/controls/controls.tsx @@ -1,9 +1,8 @@ -import { useParams } from "react-router"; import React from "react"; import { AgentControlBar } from "./agent-control-bar"; import { AgentStatusBar } from "./agent-status-bar"; import { SecurityLock } from "./security-lock"; -import { useUserConversation } from "#/hooks/query/use-user-conversation"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { ConversationCard } from "../conversation-panel/conversation-card"; interface ControlsProps { @@ -12,10 +11,7 @@ interface ControlsProps { } export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) { - const params = useParams(); - const { data: conversation } = useUserConversation( - params.conversationId ?? null, - ); + const { data: conversation } = useActiveConversation(); return (
diff --git a/frontend/src/context/ws-client-provider.tsx b/frontend/src/context/ws-client-provider.tsx index 4fedab7ee3..2a3aa80a68 100644 --- a/frontend/src/context/ws-client-provider.tsx +++ b/frontend/src/context/ws-client-provider.tsx @@ -16,7 +16,7 @@ import { } from "#/types/core/actions"; import { Conversation } from "#/api/open-hands.types"; import { useUserProviders } from "#/hooks/use-user-providers"; -import { useUserConversation } from "#/hooks/query/use-user-conversation"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { OpenHandsObservation } from "#/types/core/observations"; import { isErrorObservation, @@ -68,6 +68,7 @@ const isMessageAction = ( export enum WsClientProviderStatus { CONNECTED, DISCONNECTED, + CONNECTING, } interface UseWsClient { @@ -147,7 +148,7 @@ export function WsClientProvider({ const { providers } = useUserProviders(); const messageRateHandler = useRate({ threshold: 250 }); - const { data: conversation } = useUserConversation(conversationId); + const { data: conversation } = useActiveConversation(); function send(event: Record) { if (!sioRef.current) { diff --git a/frontend/src/hooks/query/use-active-conversation.ts b/frontend/src/hooks/query/use-active-conversation.ts new file mode 100644 index 0000000000..e2b52b1c61 --- /dev/null +++ b/frontend/src/hooks/query/use-active-conversation.ts @@ -0,0 +1,14 @@ +import { useConversationId } from "#/hooks/use-conversation-id"; +import { useUserConversation } from "./use-user-conversation"; + +const FIVE_MINUTES = 1000 * 60 * 5; + +export const useActiveConversation = () => { + const { conversationId } = useConversationId(); + return useUserConversation(conversationId, (query) => { + if (query.state.data?.status === "STARTING") { + return 2000; // 2 seconds + } + return FIVE_MINUTES; + }); +}; diff --git a/frontend/src/hooks/query/use-active-host.ts b/frontend/src/hooks/query/use-active-host.ts index 297f988d79..3371ab6001 100644 --- a/frontend/src/hooks/query/use-active-host.ts +++ b/frontend/src/hooks/query/use-active-host.ts @@ -6,12 +6,16 @@ import OpenHands from "#/api/open-hands"; import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state"; import { RootState } from "#/store"; import { useConversationId } from "#/hooks/use-conversation-id"; +import { useActiveConversation } from "./use-active-conversation"; export const useActiveHost = () => { const { curAgentState } = useSelector((state: RootState) => state.agent); const [activeHost, setActiveHost] = React.useState(null); - const { conversationId } = useConversationId(); + const { data: conversation } = useActiveConversation(); + const enabled = + conversation?.status === "RUNNING" && + RUNTIME_INACTIVE_STATES.includes(curAgentState); const { data } = useQuery({ queryKey: [conversationId, "hosts"], @@ -19,7 +23,7 @@ export const useActiveHost = () => { const hosts = await OpenHands.getWebHosts(conversationId); return { hosts }; }, - enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState), + enabled, initialData: { hosts: [] }, meta: { disableToast: true, @@ -37,7 +41,7 @@ export const useActiveHost = () => { return ""; } }, - refetchInterval: 3000, + // refetchInterval: 3000, meta: { disableToast: true, }, diff --git a/frontend/src/hooks/query/use-get-git-changes.ts b/frontend/src/hooks/query/use-get-git-changes.ts index 56a412edc4..b314c47470 100644 --- a/frontend/src/hooks/query/use-get-git-changes.ts +++ b/frontend/src/hooks/query/use-get-git-changes.ts @@ -6,14 +6,18 @@ import { useConversationId } from "#/hooks/use-conversation-id"; import { GitChange } from "#/api/open-hands.types"; import { RootState } from "#/store"; import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state"; +import { useActiveConversation } from "./use-active-conversation"; export const useGetGitChanges = () => { const { conversationId } = useConversationId(); + const { data: conversation } = useActiveConversation(); const [orderedChanges, setOrderedChanges] = React.useState([]); const previousDataRef = React.useRef(null); const { curAgentState } = useSelector((state: RootState) => state.agent); - const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState); + const enabled = + conversation?.status === "RUNNING" && + RUNTIME_INACTIVE_STATES.includes(curAgentState); const result = useQuery({ queryKey: ["file_changes", conversationId], @@ -21,7 +25,7 @@ export const useGetGitChanges = () => { retry: false, staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 15, // 15 minutes - enabled: runtimeIsActive, + enabled, meta: { disableToast: true, }, diff --git a/frontend/src/hooks/query/use-user-conversation.ts b/frontend/src/hooks/query/use-user-conversation.ts index 82653f4a25..d424adeda7 100644 --- a/frontend/src/hooks/query/use-user-conversation.ts +++ b/frontend/src/hooks/query/use-user-conversation.ts @@ -1,10 +1,24 @@ -import { useQuery } from "@tanstack/react-query"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Query, useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; import OpenHands from "#/api/open-hands"; +import { Conversation } from "#/api/open-hands.types"; const FIVE_MINUTES = 1000 * 60 * 5; const FIFTEEN_MINUTES = 1000 * 60 * 15; +type RefetchInterval = ( + query: Query< + Conversation | null, + AxiosError, + Conversation | null, + (string | null)[] + >, +) => number; -export const useUserConversation = (cid: string | null) => +export const useUserConversation = ( + cid: string | null, + refetchInterval?: RefetchInterval, +) => useQuery({ queryKey: ["user", "conversation", cid], queryFn: async () => { @@ -14,12 +28,7 @@ export const useUserConversation = (cid: string | null) => }, enabled: !!cid, retry: false, - refetchInterval: (query) => { - if (query.state.data?.status === "STARTING") { - return 2000; // 2 seconds - } - return FIVE_MINUTES; - }, + refetchInterval, staleTime: FIVE_MINUTES, gcTime: FIFTEEN_MINUTES, }); diff --git a/frontend/src/hooks/query/use-vscode-url.ts b/frontend/src/hooks/query/use-vscode-url.ts index 564d71ef43..0a9311469e 100644 --- a/frontend/src/hooks/query/use-vscode-url.ts +++ b/frontend/src/hooks/query/use-vscode-url.ts @@ -7,6 +7,7 @@ import { I18nKey } from "#/i18n/declaration"; import { RootState } from "#/store"; import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state"; import { transformVSCodeUrl } from "#/utils/vscode-url-helper"; +import { useActiveConversation } from "./use-active-conversation"; // Define the return type for the VS Code URL query interface VSCodeUrlResult { @@ -17,8 +18,11 @@ interface VSCodeUrlResult { export const useVSCodeUrl = () => { const { t } = useTranslation(); const { conversationId } = useConversationId(); + const { data: conversation } = useActiveConversation(); const { curAgentState } = useSelector((state: RootState) => state.agent); - const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState); + const enabled = + conversation?.status === "RUNNING" && + RUNTIME_INACTIVE_STATES.includes(curAgentState); return useQuery({ queryKey: ["vscode_url", conversationId], @@ -36,7 +40,7 @@ export const useVSCodeUrl = () => { error: t(I18nKey.VSCODE$URL_NOT_AVAILABLE), }; }, - enabled: !!conversationId && !isRuntimeInactive, + enabled, refetchOnMount: true, retry: 3, }); diff --git a/frontend/src/hooks/use-document-title-from-state.ts b/frontend/src/hooks/use-document-title-from-state.ts index aaf968bf80..3882debb26 100644 --- a/frontend/src/hooks/use-document-title-from-state.ts +++ b/frontend/src/hooks/use-document-title-from-state.ts @@ -1,6 +1,5 @@ -import { useParams } from "react-router"; import { useEffect, useRef } from "react"; -import { useUserConversation } from "./query/use-user-conversation"; +import { useActiveConversation } from "./query/use-active-conversation"; /** * Hook that updates the document title based on the current conversation. @@ -9,10 +8,7 @@ import { useUserConversation } from "./query/use-user-conversation"; * @param suffix Optional suffix to append to the title (default: "OpenHands") */ export function useDocumentTitleFromState(suffix = "OpenHands") { - const params = useParams(); - const { data: conversation } = useUserConversation( - params.conversationId ?? null, - ); + const { data: conversation } = useActiveConversation(); const lastValidTitleRef = useRef(null); useEffect(() => { diff --git a/frontend/src/routes/conversation.tsx b/frontend/src/routes/conversation.tsx index 24b6b46e6a..babd001646 100644 --- a/frontend/src/routes/conversation.tsx +++ b/frontend/src/routes/conversation.tsx @@ -27,7 +27,7 @@ import { ResizablePanel, } from "#/components/layout/resizable-panel"; import Security from "#/components/shared/modals/security/security"; -import { useUserConversation } from "#/hooks/query/use-user-conversation"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { ServedAppLabel } from "#/components/layout/served-app-label"; import { useSettings } from "#/hooks/query/use-settings"; import { RootState } from "#/store"; @@ -42,9 +42,7 @@ function AppContent() { const { t } = useTranslation(); const { data: settings } = useSettings(); const { conversationId } = useConversationId(); - const { data: conversation, isFetched } = useUserConversation( - conversationId || null, - ); + const { data: conversation, isFetched } = useActiveConversation(); const { curAgentState } = useSelector((state: RootState) => state.agent); const dispatch = useDispatch(); diff --git a/openhands/runtime/impl/docker/docker_runtime.py b/openhands/runtime/impl/docker/docker_runtime.py index cbde7410d8..e783004e4b 100644 --- a/openhands/runtime/impl/docker/docker_runtime.py +++ b/openhands/runtime/impl/docker/docker_runtime.py @@ -147,7 +147,7 @@ class DockerRuntime(ActionExecutionClient): except docker.errors.NotFound as e: if self.attach_to_existing: self.log( - 'error', + 'warning', f'Container {self.container_name} not found.', ) raise AgentRuntimeDisconnectedError from e