refactor(frontend): move conversation APIs to a dedicated service handler (#10957)

This commit is contained in:
Hiep Le 2025-09-16 00:57:15 +07:00 committed by GitHub
parent 10b871f4ab
commit 3f984d878b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 223 additions and 163 deletions

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import {
FILE_VARIANTS_1,
FILE_VARIANTS_2,
@ -10,20 +10,20 @@ import {
* You can find the mock handlers in `frontend/src/mocks/file-service-handlers.ts`.
*/
describe("OpenHands File API", () => {
describe("ConversationService File API", () => {
it("should get a list of files", async () => {
await expect(OpenHands.getFiles("test-conversation-id")).resolves.toEqual(
FILE_VARIANTS_1,
);
await expect(
ConversationService.getFiles("test-conversation-id"),
).resolves.toEqual(FILE_VARIANTS_1);
await expect(
OpenHands.getFiles("test-conversation-id-2"),
ConversationService.getFiles("test-conversation-id-2"),
).resolves.toEqual(FILE_VARIANTS_2);
});
it("should get content of a file", async () => {
await expect(
OpenHands.getFile("test-conversation-id", "file1.txt"),
ConversationService.getFile("test-conversation-id", "file1.txt"),
).resolves.toEqual("Content of file1.txt");
});
});

View File

@ -8,7 +8,7 @@ import {
UserMessageAction,
} from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { Conversation } from "#/api/open-hands.types";
vi.mock("react-router", () => ({
@ -80,7 +80,7 @@ describe("Messages", () => {
});
it("should render a launch to microagent action button on chat messages only if it is a user message", () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
const getConversationSpy = vi.spyOn(ConversationService, "getConversation");
const mockConversation: Conversation = {
conversation_id: "123",
title: "Test Conversation",

View File

@ -6,7 +6,7 @@ import { createRoutesStub } from "react-router";
import React from "react";
import { renderWithProviders } from "test-utils";
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { Conversation } from "#/api/open-hands.types";
describe("ConversationPanel", () => {
@ -85,7 +85,7 @@ describe("ConversationPanel", () => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Setup default mock for getUserConversations
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue({
vi.spyOn(ConversationService, "getUserConversations").mockResolvedValue({
results: [...mockConversations],
next_page_id: null,
});
@ -101,7 +101,10 @@ describe("ConversationPanel", () => {
});
it("should display an empty state when there are no conversations", async () => {
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockResolvedValue({
results: [],
next_page_id: null,
@ -114,7 +117,10 @@ describe("ConversationPanel", () => {
});
it("should handle an error when fetching conversations", async () => {
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockRejectedValue(
new Error("Failed to fetch conversations"),
);
@ -203,14 +209,17 @@ describe("ConversationPanel", () => {
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockImplementation(async () => ({
results: mockData,
next_page_id: null,
}));
const deleteUserConversationSpy = vi.spyOn(
OpenHands,
ConversationService,
"deleteUserConversation",
);
deleteUserConversationSpy.mockImplementation(async (id: string) => {
@ -260,7 +269,10 @@ describe("ConversationPanel", () => {
it("should refetch data on rerenders", async () => {
const user = userEvent.setup();
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockResolvedValue({
results: [...mockConversations],
next_page_id: null,
@ -357,7 +369,10 @@ describe("ConversationPanel", () => {
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockResolvedValue({
results: mockRunningConversations,
next_page_id: null,
@ -424,13 +439,19 @@ describe("ConversationPanel", () => {
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockImplementation(async () => ({
results: mockData,
next_page_id: null,
}));
const stopConversationSpy = vi.spyOn(OpenHands, "stopConversation");
const stopConversationSpy = vi.spyOn(
ConversationService,
"stopConversation",
);
stopConversationSpy.mockImplementation(async (id: string) => {
const conversation = mockData.find((conv) => conv.conversation_id === id);
if (conversation) {
@ -512,7 +533,10 @@ describe("ConversationPanel", () => {
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
const getUserConversationsSpy = vi.spyOn(
ConversationService,
"getUserConversations",
);
getUserConversationsSpy.mockResolvedValue({
results: mockMixedStatusConversations,
next_page_id: null,
@ -619,7 +643,10 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
// Mock the updateConversation API call
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockResolvedValue(true);
// Mock the toast function
@ -656,7 +683,10 @@ describe("ConversationPanel", () => {
it("should save title when Enter key is pressed", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
@ -685,7 +715,10 @@ describe("ConversationPanel", () => {
it("should trim whitespace from title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
@ -714,7 +747,10 @@ describe("ConversationPanel", () => {
it("should revert to original title when empty", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
@ -740,7 +776,10 @@ describe("ConversationPanel", () => {
it("should handle API error when updating title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockRejectedValue(new Error("API Error"));
vi.mock("#/utils/custom-toast-handlers", () => ({
@ -807,7 +846,10 @@ describe("ConversationPanel", () => {
it("should not call API when title is unchanged", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
@ -833,7 +875,10 @@ describe("ConversationPanel", () => {
it("should handle special characters in title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
const updateConversationSpy = vi.spyOn(
ConversationService,
"updateConversation",
);
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();

View File

@ -5,8 +5,8 @@ import { createRoutesStub } from "react-router";
import { setupStore } from "test-utils";
import { describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { NewConversation } from "#/components/features/home/new-conversation/new-conversation";
import OpenHands from "#/api/open-hands";
// Mock the translation function
vi.mock("react-i18next", async () => {
@ -54,7 +54,10 @@ const renderNewConversation = () => {
describe("NewConversation", () => {
it("should create an empty conversation and redirect when pressing the launch from scratch button", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const createConversationSpy = vi.spyOn(
ConversationService,
"createConversation",
);
renderNewConversation();

View File

@ -6,7 +6,7 @@ import { setupStore } from "test-utils";
import { Provider } from "react-redux";
import { createRoutesStub, Outlet } from "react-router";
import SettingsService from "#/settings-service/settings-service.api";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import GitService from "#/api/git-service/git-service.api";
import OptionService from "#/api/option-service/option-service.api";
import { GitRepository } from "#/types/git";
@ -315,7 +315,10 @@ describe("RepoConnector", () => {
});
it("should create a conversation and redirect with the selected repo when pressing the launch button", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const createConversationSpy = vi.spyOn(
ConversationService,
"createConversation",
);
createConversationSpy.mockResolvedValue({
conversation_id: "mock-conversation-id",
title: "Test Conversation",
@ -400,7 +403,10 @@ describe("RepoConnector", () => {
});
it("should change the launch button text to 'Loading...' when creating a conversation", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const createConversationSpy = vi.spyOn(
ConversationService,
"createConversation",
);
createConversationSpy.mockImplementation(() => new Promise(() => {})); // Never resolves to keep loading state
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,

View File

@ -2,7 +2,6 @@ import { render, screen } from "@testing-library/react";
import { describe, expect, vi, beforeEach, it } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RepositorySelectionForm } from "../../../../src/components/features/home/repo-selection-form";
import OpenHands from "#/api/open-hands";
import UserService from "#/api/user-service/user-service.api";
import GitService from "#/api/git-service/git-service.api";
import { GitRepository } from "#/types/git";

View File

@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event";
import { Provider } from "react-redux";
import { createRoutesStub } from "react-router";
import { setupStore } from "test-utils";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import UserService from "#/api/user-service/user-service.api";
import GitService from "#/api/git-service/git-service.api";
import { TaskCard } from "#/components/features/home/tasks/task-card";
@ -59,7 +59,10 @@ describe("TaskCard", () => {
});
it("should call createConversation when clicking the launch button", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const createConversationSpy = vi.spyOn(
ConversationService,
"createConversation",
);
renderTaskCard();
@ -82,7 +85,10 @@ describe("TaskCard", () => {
});
it("should call create conversation with suggest task trigger and selected suggested task", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const createConversationSpy = vi.spyOn(
ConversationService,
"createConversation",
);
renderTaskCard(MOCK_TASK_1);
@ -108,7 +114,10 @@ describe("TaskCard", () => {
});
it("should navigate to the conversation page after creating a conversation", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const createConversationSpy = vi.spyOn(
ConversationService,
"createConversation",
);
createConversationSpy.mockResolvedValue({
conversation_id: "test-conversation-id",
title: "Test Conversation",

View File

@ -7,7 +7,7 @@ import React from "react";
import { renderWithProviders } from "test-utils";
import MicroagentManagement from "#/routes/microagent-management";
import { MicroagentManagementMain } from "#/components/features/microagent-management/microagent-management-main";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import GitService from "#/api/git-service/git-service.api";
import { GitRepository } from "#/types/git";
import { RepositoryMicroagent } from "#/types/microagent-management";
@ -241,7 +241,7 @@ describe("MicroagentManagement", () => {
...mockMicroagents,
]);
// Setup default mock for searchConversations
vi.spyOn(OpenHands, "searchConversations").mockResolvedValue([
vi.spyOn(ConversationService, "searchConversations").mockResolvedValue([
...mockConversations,
]);
// Setup default mock for getRepositoryMicroagentContent

View File

@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { AgentState } from "#/types/agent-state";
vi.mock("react-redux", async () => {
@ -48,7 +48,7 @@ describe("MicroagentsModal - Refresh Button", () => {
vi.clearAllMocks();
// Setup default mock for getUserConversations
vi.spyOn(OpenHands, "getMicroagents").mockResolvedValue({
vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({
microagents: mockMicroagents,
});
});
@ -73,7 +73,7 @@ describe("MicroagentsModal - Refresh Button", () => {
renderWithProviders(<MicroagentsModal {...defaultProps} />);
const refreshSpy = vi.spyOn(OpenHands, "getMicroagents");
const refreshSpy = vi.spyOn(ConversationService, "getMicroagents");
const refreshButton = screen.getByTestId("refresh-microagents");
await user.click(refreshButton);

View File

@ -11,7 +11,6 @@ import i18n from "#/i18n";
import OptionService from "#/api/option-service/option-service.api";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import SettingsService from "#/settings-service/settings-service.api";
import OpenHands from "#/api/open-hands";
import * as ToastHandlers from "#/utils/custom-toast-handlers";
describe("frontend/routes/_oh", () => {

View File

@ -7,7 +7,6 @@ import i18next from "i18next";
import { I18nextProvider } from "react-i18next";
import GitSettingsScreen from "#/routes/git-settings";
import SettingsService from "#/settings-service/settings-service.api";
import OpenHands from "#/api/open-hands";
import OptionService from "#/api/option-service/option-service.api";
import AuthService from "#/api/auth-service/auth-service.api";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";

View File

@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import LlmSettingsScreen from "#/routes/llm-settings";
import SettingsService from "#/settings-service/settings-service.api";
import OpenHands from "#/api/open-hands";
import OptionService from "#/api/option-service/option-service.api";
import {
MOCK_DEFAULT_USER_SETTINGS,

View File

@ -7,7 +7,6 @@ import SecretsSettingsScreen from "#/routes/secrets-settings";
import { SecretsService } from "#/api/secrets-service";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import SettingsService from "#/settings-service/settings-service.api";
import OpenHands from "#/api/open-hands";
import OptionService from "#/api/option-service/option-service.api";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";

View File

@ -3,14 +3,15 @@ import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createRoutesStub } from "react-router";
import { renderWithProviders } from "test-utils";
import OpenHands from "#/api/open-hands";
import SettingsScreen from "#/routes/settings";
import { PaymentForm } from "#/components/features/payment/payment-form";
import * as useSettingsModule from "#/hooks/query/use-settings";
// Mock the useSettings hook
vi.mock("#/hooks/query/use-settings", async () => {
const actual = await vi.importActual<typeof import("#/hooks/query/use-settings")>("#/hooks/query/use-settings");
const actual = await vi.importActual<
typeof import("#/hooks/query/use-settings")
>("#/hooks/query/use-settings");
return {
...actual,
useSettings: vi.fn().mockReturnValue({
@ -24,21 +25,22 @@ vi.mock("#/hooks/query/use-settings", async () => {
// Mock the i18next hook
vi.mock("react-i18next", async () => {
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
const actual =
await vi.importActual<typeof import("react-i18next")>("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"SETTINGS$NAV_INTEGRATIONS": "Integrations",
"SETTINGS$NAV_APPLICATION": "Application",
"SETTINGS$NAV_CREDITS": "Credits",
"SETTINGS$NAV_API_KEYS": "API Keys",
"SETTINGS$NAV_LLM": "LLM",
"SETTINGS$NAV_USER": "User",
"SETTINGS$NAV_SECRETS": "Secrets",
"SETTINGS$NAV_MCP": "MCP",
"SETTINGS$TITLE": "Settings"
SETTINGS$NAV_INTEGRATIONS: "Integrations",
SETTINGS$NAV_APPLICATION: "Application",
SETTINGS$NAV_CREDITS: "Credits",
SETTINGS$NAV_API_KEYS: "API Keys",
SETTINGS$NAV_LLM: "LLM",
SETTINGS$NAV_USER: "User",
SETTINGS$NAV_SECRETS: "Secrets",
SETTINGS$NAV_MCP: "MCP",
SETTINGS$TITLE: "Settings",
};
return translations[key] || key;
},

View File

@ -3,7 +3,6 @@ import { createRoutesStub } from "react-router";
import { describe, expect, it, vi } from "vitest";
import { QueryClientProvider } from "@tanstack/react-query";
import SettingsScreen, { clientLoader } from "#/routes/settings";
import OpenHands from "#/api/open-hands";
import OptionService from "#/api/option-service/option-service.api";
// Mock the i18next hook

View File

@ -6,21 +6,19 @@ import {
Conversation,
ResultSet,
GetTrajectoryResponse,
GitChangeDiff,
GitChange,
GetMicroagentsResponse,
GetMicroagentPromptResponse,
CreateMicroagent,
FileUploadSuccessResponse,
GetFilesResponse,
GetFileResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
} from "../open-hands.types";
import { openHands } from "../open-hands-axios";
import { Provider } from "#/types/settings";
import { SuggestedTask } from "#/utils/types";
import { BatchFeedbackData } from "#/hooks/query/use-batch-feedback";
class OpenHands {
class ConversationService {
private static currentConversation: Conversation | null = null;
/**
@ -322,30 +320,6 @@ class OpenHands {
return data;
}
static async getGitChanges(conversationId: string): Promise<GitChange[]> {
const url = `${this.getConversationUrl(conversationId)}/git/changes`;
const { data } = await openHands.get<GitChange[]>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
static async getGitChangeDiff(
conversationId: string,
path: string,
): Promise<GitChangeDiff> {
const url = `${this.getConversationUrl(conversationId)}/git/diff`;
const { data } = await openHands.get<GitChangeDiff>(url, {
params: { path },
headers: this.getConversationHeaders(),
});
return data;
}
/**
* @returns A list of repositories
*/
/**
* Get the available microagents associated with a conversation
* @param conversationId The ID of the conversation
@ -361,21 +335,6 @@ class OpenHands {
return data;
}
/**
* Get the available microagents for a repository
* @param owner The repository owner
* @param repo The repository name
* @returns The available microagents for the repository
*/
/**
* Get the content of a specific microagent from a repository
* @param owner The repository owner
* @param repo The repository name
* @param filePath The path to the microagent file within the repository
* @returns The microagent content and metadata
*/
static async getMicroagentPrompt(
conversationId: string,
eventId: number,
@ -465,4 +424,4 @@ class OpenHands {
}
}
export default OpenHands;
export default ConversationService;

View File

@ -1,4 +1,4 @@
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
/**
* Returns a URL compatible for the file service
@ -6,4 +6,4 @@ import OpenHands from "#/api/open-hands";
* @returns URL of the conversation
*/
export const getConversationUrl = (conversationId: string) =>
OpenHands.getConversationUrl(conversationId);
ConversationService.getConversationUrl(conversationId);

View File

@ -3,7 +3,12 @@ import { Provider } from "#/types/settings";
import { GitRepository, PaginatedBranchesResponse, Branch } from "#/types/git";
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
import { RepositoryMicroagent } from "#/types/microagent-management";
import { MicroagentContentResponse } from "../open-hands.types";
import {
MicroagentContentResponse,
GitChange,
GitChangeDiff,
} from "../open-hands.types";
import ConversationService from "../conversation-service/conversation-service.api";
/**
* Git Service API - Handles all Git-related API endpoints
@ -210,6 +215,37 @@ class GitService {
);
return data;
}
/**
* Get git changes for a conversation
* @param conversationId The conversation ID
* @returns List of git changes
*/
static async getGitChanges(conversationId: string): Promise<GitChange[]> {
const url = `${ConversationService.getConversationUrl(conversationId)}/git/changes`;
const { data } = await openHands.get<GitChange[]>(url, {
headers: ConversationService.getConversationHeaders(),
});
return data;
}
/**
* Get git change diff for a specific file
* @param conversationId The conversation ID
* @param path The file path
* @returns Git change diff
*/
static async getGitChangeDiff(
conversationId: string,
path: string,
): Promise<GitChangeDiff> {
const url = `${ConversationService.getConversationUrl(conversationId)}/git/diff`;
const { data } = await openHands.get<GitChangeDiff>(url, {
params: { path },
headers: ConversationService.getConversationHeaders(),
});
return data;
}
}
export default GitService;

View File

@ -13,7 +13,7 @@ import { BaseModal } from "../../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useWsClient } from "#/context/ws-client-provider";
import { isSystemMessage } from "#/types/core/guards";
import { ConversationStatus } from "#/types/conversation-status";
@ -108,7 +108,7 @@ export function ConversationCard({
// Fetch the VS Code URL from the API
if (conversationId) {
try {
const data = await OpenHands.getVSCodeUrl(conversationId);
const data = await ConversationService.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(data.vscode_url);
if (transformedUrl) {

View File

@ -5,7 +5,7 @@ import { I18nKey } from "#/i18n/declaration";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import { useConversationId } from "#/hooks/use-conversation-id";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { RootState } from "#/store";
export function VSCodeTooltipContent() {
@ -20,7 +20,7 @@ export function VSCodeTooltipContent() {
if (conversationId) {
try {
const data = await OpenHands.getVSCodeUrl(conversationId);
const data = await ConversationService.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(data.vscode_url);
if (transformedUrl) {

View File

@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { SuggestedTask } from "#/utils/types";
import { Provider } from "#/types/settings";
import { CreateMicroagent } from "#/api/open-hands.types";
@ -31,7 +31,7 @@ export const useCreateConversation = () => {
createMicroagent,
} = variables;
return OpenHands.createConversation(
return ConversationService.createConversation(
repository?.name,
repository?.gitProvider,
query,

View File

@ -1,12 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useDeleteConversation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { conversationId: string }) =>
OpenHands.deleteUserConversation(variables.conversationId),
ConversationService.deleteUserConversation(variables.conversationId),
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: ["user", "conversations"] });
const previousConversations = queryClient.getQueryData([

View File

@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useGetTrajectory = () =>
useMutation({
mutationFn: (cid: string) => OpenHands.getTrajectory(cid),
mutationFn: (cid: string) => ConversationService.getTrajectory(cid),
});

View File

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { Provider } from "#/types/settings";
export const useStartConversation = () => {
@ -10,7 +10,7 @@ export const useStartConversation = () => {
conversationId: string;
providers?: Provider[];
}) =>
OpenHands.startConversation(
ConversationService.startConversation(
variables.conversationId,
variables.providers,
),

View File

@ -1,12 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useStopConversation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { conversationId: string }) =>
OpenHands.stopConversation(variables.conversationId),
ConversationService.stopConversation(variables.conversationId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ["user", "conversations"] });
const previousConversations = queryClient.getQueryData([

View File

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "#/hooks/use-conversation-id";
import {
BatchFeedbackData,
@ -18,7 +18,7 @@ export const useSubmitConversationFeedback = () => {
return useMutation({
mutationFn: ({ rating, eventId, reason }: SubmitConversationFeedbackArgs) =>
OpenHands.submitConversationFeedback(
ConversationService.submitConversationFeedback(
conversationId,
rating,
eventId,

View File

@ -1,6 +1,6 @@
import { useMutation } from "@tanstack/react-query";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "#/hooks/use-conversation-id";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
@ -12,7 +12,7 @@ export const useSubmitFeedback = () => {
const { conversationId } = useConversationId();
return useMutation({
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
OpenHands.submitFeedback(conversationId, feedback),
ConversationService.submitFeedback(conversationId, feedback),
onError: (error) => {
displayErrorToast(error.message);
},

View File

@ -1,12 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useUpdateConversation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { conversationId: string; newTitle: string }) =>
OpenHands.updateConversation(variables.conversationId, {
ConversationService.updateConversation(variables.conversationId, {
title: variables.newTitle,
}),
onMutate: async (variables) => {

View File

@ -1,11 +1,14 @@
import { useMutation } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useUploadFiles = () =>
useMutation({
mutationKey: ["upload-files"],
mutationFn: (variables: { conversationId: string; files: File[] }) =>
OpenHands.uploadFiles(variables.conversationId!, variables.files),
ConversationService.uploadFiles(
variables.conversationId!,
variables.files,
),
onSuccess: async () => {},
meta: {
disableToast: true,

View File

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useUserConversation } from "./use-user-conversation";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useActiveConversation = () => {
const { conversationId } = useConversationId();
@ -16,7 +16,7 @@ export const useActiveConversation = () => {
useEffect(() => {
const conversation = userConversation.data;
OpenHands.setCurrentConversation(conversation || null);
ConversationService.setCurrentConversation(conversation || null);
}, [
conversationId,
userConversation.isFetched,

View File

@ -1,7 +1,7 @@
import { useQueries, useQuery } from "@tanstack/react-query";
import axios from "axios";
import React from "react";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
@ -13,7 +13,7 @@ export const useActiveHost = () => {
const { data } = useQuery({
queryKey: [conversationId, "hosts"],
queryFn: async () => {
const hosts = await OpenHands.getWebHosts(conversationId);
const hosts = await ConversationService.getWebHosts(conversationId);
return { hosts };
},
enabled: runtimeIsReady && !!conversationId,

View File

@ -1,6 +1,6 @@
import React from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useConfig } from "#/hooks/query/use-config";
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
@ -30,7 +30,7 @@ export const useBatchFeedback = () => {
const query = useQuery({
queryKey: getFeedbackQueryKey(conversationId),
queryFn: () => OpenHands.getBatchFeedback(conversationId!),
queryFn: () => ConversationService.getBatchFeedback(conversationId!),
enabled: runtimeIsReady && !!conversationId && config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes

View File

@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useConversationId } from "#/hooks/use-conversation-id";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useRuntimeIsReady } from "../use-runtime-is-ready";
export const useConversationConfig = () => {
@ -12,7 +12,7 @@ export const useConversationConfig = () => {
queryKey: ["conversation_config", conversationId],
queryFn: () => {
if (!conversationId) throw new Error("No conversation ID");
return OpenHands.getRuntimeId(conversationId);
return ConversationService.getRuntimeId(conversationId);
},
enabled: runtimeIsReady && !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes

View File

@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "../use-conversation-id";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
@ -15,7 +15,7 @@ export const useConversationMicroagents = () => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
const data = await OpenHands.getMicroagents(conversationId);
const data = await ConversationService.getMicroagents(conversationId);
return data.microagents;
},
enabled:

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import GitService from "#/api/git-service/git-service.api";
import { GitChangeStatus } from "#/api/open-hands.types";
import { useConversationId } from "#/hooks/use-conversation-id";
@ -14,7 +14,7 @@ export const useGitDiff = (config: UseGetDiffConfig) => {
return useQuery({
queryKey: ["file_diff", conversationId, config.filePath, config.type],
queryFn: () => OpenHands.getGitChangeDiff(conversationId, config.filePath),
queryFn: () => GitService.getGitChangeDiff(conversationId, config.filePath),
enabled: config.enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes

View File

@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import OpenHands from "#/api/open-hands";
import GitService from "#/api/git-service/git-service.api";
import { useConversationId } from "#/hooks/use-conversation-id";
import { GitChange } from "#/api/open-hands.types";
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
@ -13,7 +13,7 @@ export const useGetGitChanges = () => {
const result = useQuery({
queryKey: ["file_changes", conversationId],
queryFn: () => OpenHands.getGitChanges(conversationId),
queryFn: () => GitService.getGitChanges(conversationId),
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes

View File

@ -1,13 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { useConversationId } from "../use-conversation-id";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useGetMicroagents = (microagentDirectory: string) => {
const { conversationId } = useConversationId();
return useQuery({
queryKey: ["files", "microagents", conversationId, microagentDirectory],
queryFn: () => OpenHands.getFiles(conversationId!, microagentDirectory),
queryFn: () =>
ConversationService.getFiles(conversationId!, microagentDirectory),
enabled: !!conversationId,
select: (data) =>
data.map((fileName) => fileName.replace(microagentDirectory, "")),

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "../use-conversation-id";
export const useMicroagentPrompt = (eventId: number) => {
@ -7,7 +7,8 @@ export const useMicroagentPrompt = (eventId: number) => {
return useQuery({
queryKey: ["memory", "prompt", conversationId, eventId],
queryFn: () => OpenHands.getMicroagentPrompt(conversationId!, eventId),
queryFn: () =>
ConversationService.getMicroagentPrompt(conversationId!, eventId),
enabled: !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes

View File

@ -1,5 +1,5 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useIsAuthed } from "./use-is-authed";
export const usePaginatedConversations = (limit: number = 20) => {
@ -8,7 +8,7 @@ export const usePaginatedConversations = (limit: number = 20) => {
return useInfiniteQuery({
queryKey: ["user", "conversations", "paginated", limit],
queryFn: ({ pageParam }) =>
OpenHands.getUserConversations(limit, pageParam),
ConversationService.getUserConversations(limit, pageParam),
enabled: !!userIsAuthenticated,
getNextPageParam: (lastPage) => lastPage.next_page_id,
initialPageParam: undefined as string | undefined,

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
export const useSearchConversations = (
selectedRepository?: string,
@ -16,7 +16,7 @@ export const useSearchConversations = (
limit,
],
queryFn: () =>
OpenHands.searchConversations(
ConversationService.searchConversations(
selectedRepository,
conversationTrigger,
limit,

View File

@ -1,7 +1,7 @@
/* 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 ConversationService from "#/api/conversation-service/conversation-service.api";
import { Conversation } from "#/api/open-hands.types";
const FIVE_MINUTES = 1000 * 60 * 5;
@ -22,7 +22,7 @@ export const useUserConversation = (
useQuery({
queryKey: ["user", "conversation", cid],
queryFn: async () => {
const conversation = await OpenHands.getConversation(cid!);
const conversation = await ConversationService.getConversation(cid!);
return conversation;
},
enabled: !!cid,

View File

@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "#/hooks/use-conversation-id";
import { I18nKey } from "#/i18n/declaration";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
@ -21,7 +21,7 @@ export const useVSCodeUrl = () => {
queryKey: ["vscode_url", conversationId],
queryFn: async () => {
if (!conversationId) throw new Error("No conversation ID");
const data = await OpenHands.getVSCodeUrl(conversationId);
const data = await ConversationService.getVSCodeUrl(conversationId);
if (data.vscode_url) {
return {
url: transformVSCodeUrl(data.vscode_url),

View File

@ -8,7 +8,7 @@ import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import { RootState } from "#/store";
import { isSystemMessage } from "#/types/core/guards";
import { ConversationStatus } from "#/types/conversation-status";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useDeleteConversation } from "./mutation/use-delete-conversation";
import { useStopConversation } from "./mutation/use-stop-conversation";
import { useGetTrajectory } from "./mutation/use-get-trajectory";
@ -129,7 +129,7 @@ export function useConversationNameContextMenu({
// Fetch the VS Code URL from the API
if (conversationId) {
try {
const data = await OpenHands.getVSCodeUrl(conversationId);
const data = await ConversationService.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(data.vscode_url);
if (transformedUrl) {

View File

@ -7,7 +7,7 @@ import { useUserProviders } from "./use-user-providers";
import { useConversationSubscriptions } from "#/context/conversation-subscriptions-provider";
import { Provider } from "#/types/settings";
import { CreateMicroagent, Conversation } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { renderConversationStartingToast } from "#/components/features/chat/microagent/microagent-status-toast";
interface ConversationData {
@ -45,7 +45,7 @@ export const useCreateConversationAndSubscribeMultiple = () => {
const conversationQueries = useQueries({
queries: conversationIdsToWatch.map((conversationId) => ({
queryKey: ["conversation-ready-poll", conversationId],
queryFn: () => OpenHands.getConversation(conversationId),
queryFn: () => ConversationService.getConversation(conversationId),
enabled: !!conversationId,
refetchInterval: (query: Query<Conversation | null, AxiosError>) => {
const status = query.state.data?.status;

View File

@ -19,7 +19,7 @@ import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
import { ConversationSubscriptionsProvider } from "#/context/conversation-subscriptions-provider";
import { useUserProviders } from "#/hooks/use-user-providers";
@ -53,9 +53,10 @@ function AppContent() {
navigate("/");
} else if (conversation?.status === "STOPPED") {
// start the conversation if the state is stopped on initial load
OpenHands.startConversation(conversation.conversation_id, providers).then(
() => refetch(),
);
ConversationService.startConversation(
conversation.conversation_id,
providers,
).then(() => refetch());
}
}, [conversation?.conversation_id, isFetched, isAuthed, providers]);