mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
fix: subscription logic by polling for available runtime (microagent management, memory UI) (#10519)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com> Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -14,21 +14,31 @@ import { Conversation } from "#/api/open-hands.types";
|
||||
|
||||
// Mock hooks
|
||||
const mockUseUserProviders = vi.fn();
|
||||
const mockUseUserRepositories = vi.fn();
|
||||
const mockUseGitRepositories = vi.fn();
|
||||
const mockUseConfig = vi.fn();
|
||||
const mockUseRepositoryMicroagents = vi.fn();
|
||||
const mockUseSearchConversations = vi.fn();
|
||||
|
||||
vi.mock("#/hooks/use-user-providers", () => ({
|
||||
useUserProviders: () => mockUseUserProviders(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-user-repositories", () => ({
|
||||
useUserRepositories: () => mockUseUserRepositories(),
|
||||
vi.mock("#/hooks/query/use-git-repositories", () => ({
|
||||
useGitRepositories: () => mockUseGitRepositories(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-config", () => ({
|
||||
useConfig: () => mockUseConfig(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-repository-microagents", () => ({
|
||||
useRepositoryMicroagents: () => mockUseRepositoryMicroagents(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-search-conversations", () => ({
|
||||
useSearchConversations: () => mockUseSearchConversations(),
|
||||
}));
|
||||
|
||||
describe("MicroagentManagement", () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
@@ -174,7 +184,7 @@ describe("MicroagentManagement", () => {
|
||||
providers: ["github"],
|
||||
});
|
||||
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
@@ -196,6 +206,18 @@ describe("MicroagentManagement", () => {
|
||||
},
|
||||
});
|
||||
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: mockMicroagents,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: mockConversations,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
// Setup default mock for retrieveUserGitRepositories
|
||||
vi.spyOn(OpenHands, "retrieveUserGitRepositories").mockResolvedValue({
|
||||
data: [...mockRepositories],
|
||||
@@ -227,7 +249,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display loading state when fetching repositories", async () => {
|
||||
// Mock loading state
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
@@ -245,7 +267,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle error when fetching repositories", async () => {
|
||||
// Mock error state
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
@@ -258,7 +280,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for the error to be handled
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,7 +289,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that tabs are rendered
|
||||
@@ -285,7 +307,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and rendered
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that repository names are displayed
|
||||
@@ -300,7 +322,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -309,10 +331,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for microagents to be fetched
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that microagents are displayed
|
||||
@@ -325,19 +344,17 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display loading state when fetching microagents", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
getRepositoryMicroagentsSpy.mockImplementation(
|
||||
() => new Promise(() => {}), // Never resolves
|
||||
);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -350,19 +367,17 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle error when fetching microagents", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
getRepositoryMicroagentsSpy.mockRejectedValue(
|
||||
new Error("Failed to fetch microagents"),
|
||||
);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -371,23 +386,23 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for the error to be handled
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalledWith("user", "repo2");
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display empty state when no microagents are found", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -396,7 +411,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for microagents to be fetched
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalledWith("user", "repo2");
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that no microagents are displayed
|
||||
@@ -410,7 +425,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -419,10 +434,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for microagents to be fetched
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that microagent cards display correct information
|
||||
@@ -449,7 +461,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -468,7 +480,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -492,7 +504,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click the first add microagent button
|
||||
@@ -513,7 +525,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display empty state when no repositories are found", async () => {
|
||||
// Mock empty repositories
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
@@ -533,7 +545,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that empty state messages are displayed
|
||||
@@ -550,7 +562,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -559,14 +571,11 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for microagents to be fetched for first repo
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that the API call was made
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledTimes(1);
|
||||
// Check that the hook was called
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should display ready to add microagent message in main area", async () => {
|
||||
@@ -591,7 +600,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that search input is rendered
|
||||
@@ -611,7 +620,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Initially only repositories with .openhands should be visible
|
||||
@@ -642,7 +651,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with uppercase
|
||||
@@ -665,7 +674,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with partial match
|
||||
@@ -691,7 +700,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input
|
||||
@@ -724,7 +733,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with non-existent repository name
|
||||
@@ -752,7 +761,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with special characters
|
||||
@@ -773,7 +782,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Filter to show only repo2
|
||||
@@ -788,10 +797,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for microagents to be fetched
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that microagents are displayed
|
||||
@@ -808,7 +814,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with leading/trailing whitespace
|
||||
@@ -828,7 +834,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
@@ -860,7 +866,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -869,15 +875,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both microagents and conversations to be fetched
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(OpenHands.searchConversations).toHaveBeenCalledWith(
|
||||
"user/repo2/.openhands",
|
||||
"microagent_management",
|
||||
1000,
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -887,7 +886,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -896,8 +895,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(OpenHands.searchConversations).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that microagents are displayed
|
||||
@@ -917,23 +916,22 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should show loading state when both microagents and conversations are loading", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
|
||||
// Make both queries never resolve
|
||||
getRepositoryMicroagentsSpy.mockImplementation(
|
||||
() => new Promise(() => {}),
|
||||
);
|
||||
searchConversationsSpy.mockImplementation(() => new Promise(() => {}));
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -950,7 +948,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -959,8 +957,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(OpenHands.searchConversations).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that loading spinner is not displayed
|
||||
@@ -975,7 +973,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -984,8 +982,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(OpenHands.searchConversations).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that microagent file paths are displayed for microagents
|
||||
@@ -1010,21 +1008,22 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should show learn this repo component when no microagents and no conversations", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
|
||||
// Mock both queries to return empty arrays
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([]);
|
||||
searchConversationsSpy.mockResolvedValue([]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1033,8 +1032,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalled();
|
||||
expect(searchConversationsSpy).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that the learn this repo component is displayed
|
||||
@@ -1046,21 +1045,22 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should show learn this repo component when only conversations exist but no microagents", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
|
||||
// Mock microagents to return empty array, conversations to return data
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([]);
|
||||
searchConversationsSpy.mockResolvedValue([...mockConversations]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: [...mockConversations],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1069,8 +1069,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalled();
|
||||
expect(searchConversationsSpy).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that conversations are displayed
|
||||
@@ -1088,21 +1088,22 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should show learn this repo component when only microagents exist but no conversations", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
|
||||
// Mock microagents to return data, conversations to return empty array
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([...mockMicroagents]);
|
||||
searchConversationsSpy.mockResolvedValue([]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [...mockMicroagents],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1111,8 +1112,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalled();
|
||||
expect(searchConversationsSpy).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that microagents are displayed
|
||||
@@ -1130,16 +1131,17 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle error when fetching conversations", async () => {
|
||||
const user = userEvent.setup();
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
searchConversationsSpy.mockRejectedValue(
|
||||
new Error("Failed to fetch conversations"),
|
||||
);
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1148,11 +1150,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for the error to be handled
|
||||
await waitFor(() => {
|
||||
expect(searchConversationsSpy).toHaveBeenCalledWith(
|
||||
"user/repo2/.openhands",
|
||||
"microagent_management",
|
||||
1000,
|
||||
);
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that the learn this repo component is displayed (since conversations failed)
|
||||
@@ -1163,27 +1161,22 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Also check that the microagents query was called successfully
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle error when fetching microagents but conversations succeed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
getRepositoryMicroagentsSpy.mockRejectedValue(
|
||||
new Error("Failed to fetch microagents"),
|
||||
);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1192,10 +1185,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for the error to be handled
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that the learn this repo component is displayed (since microagents failed)
|
||||
@@ -1207,13 +1197,11 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should call searchConversations with correct parameters", async () => {
|
||||
const user = userEvent.setup();
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1222,11 +1210,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for searchConversations to be called
|
||||
await waitFor(() => {
|
||||
expect(searchConversationsSpy).toHaveBeenCalledWith(
|
||||
"user/repo2/.openhands",
|
||||
"microagent_management",
|
||||
1000,
|
||||
);
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1236,7 +1220,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1245,8 +1229,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to complete
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(OpenHands.searchConversations).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that conversations display correct information
|
||||
@@ -1263,7 +1247,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion
|
||||
@@ -1272,15 +1256,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for both queries to be called for first repo
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
expect(OpenHands.searchConversations).toHaveBeenCalledWith(
|
||||
"user/repo2/.openhands",
|
||||
"microagent_management",
|
||||
1000,
|
||||
);
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that both microagents and conversations are displayed
|
||||
@@ -1304,7 +1281,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1325,7 +1302,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1387,7 +1364,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1418,7 +1395,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1448,7 +1425,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1488,7 +1465,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1522,7 +1499,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -1555,7 +1532,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for repositories to be loaded and processed
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for repositories to be displayed in the accordion
|
||||
@@ -2409,19 +2386,22 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Setup mocks before rendering
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([]);
|
||||
searchConversationsSpy.mockResolvedValue([]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Find and click on the first repository accordion to expand it
|
||||
@@ -2430,8 +2410,8 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
// Wait for microagents and conversations to be fetched
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalled();
|
||||
expect(searchConversationsSpy).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify the learn this repo trigger is displayed when no microagents exist
|
||||
@@ -2451,19 +2431,22 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Setup mocks
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([]);
|
||||
searchConversationsSpy.mockResolvedValue([]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories and expand accordion
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const repoAccordion = screen.getByTestId("repository-name-tooltip");
|
||||
@@ -2496,35 +2479,36 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Setup mocks with existing microagents (should NOT show trigger)
|
||||
const getRepositoryMicroagentsSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"getRepositoryMicroagents",
|
||||
);
|
||||
const searchConversationsSpy = vi.spyOn(OpenHands, "searchConversations");
|
||||
|
||||
// Mock with existing microagent
|
||||
getRepositoryMicroagentsSpy.mockResolvedValue([
|
||||
{
|
||||
name: "test-microagent",
|
||||
created_at: "2021-10-01",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/test",
|
||||
},
|
||||
]);
|
||||
searchConversationsSpy.mockResolvedValue([]);
|
||||
mockUseRepositoryMicroagents.mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
name: "test-microagent",
|
||||
created_at: "2021-10-01",
|
||||
git_provider: "github",
|
||||
path: ".openhands/microagents/test",
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
mockUseSearchConversations.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseUserRepositories).toHaveBeenCalled();
|
||||
expect(mockUseGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const repoAccordion = screen.getByTestId("repository-name-tooltip");
|
||||
await user.click(repoAccordion);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getRepositoryMicroagentsSpy).toHaveBeenCalled();
|
||||
expect(searchConversationsSpy).toHaveBeenCalled();
|
||||
expect(mockUseRepositoryMicroagents).toHaveBeenCalled();
|
||||
expect(mockUseSearchConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Should NOT show the learn this repo trigger when microagents exist
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { StylesConfig } from "react-select";
|
||||
import { Provider } from "../../types/settings";
|
||||
import { ReactSelectDropdown, SelectOption } from "./react-select-dropdown";
|
||||
|
||||
@@ -11,6 +12,8 @@ export interface GitProviderDropdownProps {
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
onChange?: (provider: Provider | null) => void;
|
||||
classNamePrefix?: string;
|
||||
styles?: StylesConfig<SelectOption, false>;
|
||||
}
|
||||
|
||||
export function GitProviderDropdown({
|
||||
@@ -22,6 +25,8 @@ export function GitProviderDropdown({
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
onChange,
|
||||
classNamePrefix,
|
||||
styles,
|
||||
}: GitProviderDropdownProps) {
|
||||
const options: SelectOption[] = useMemo(
|
||||
() =>
|
||||
@@ -53,6 +58,8 @@ export function GitProviderDropdown({
|
||||
isSearchable={false}
|
||||
isLoading={isLoading}
|
||||
onChange={handleChange}
|
||||
classNamePrefix={classNamePrefix}
|
||||
styles={styles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import Select from "react-select";
|
||||
import Select, { StylesConfig } from "react-select";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { SelectOptionBase, getCustomStyles } from "./react-select-styles";
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface ReactSelectDropdownProps {
|
||||
isSearchable?: boolean;
|
||||
isLoading?: boolean;
|
||||
onChange?: (option: SelectOption | null) => void;
|
||||
classNamePrefix?: string;
|
||||
styles?: StylesConfig<SelectOption, false>;
|
||||
}
|
||||
|
||||
export function ReactSelectDropdown({
|
||||
@@ -31,6 +33,8 @@ export function ReactSelectDropdown({
|
||||
isSearchable = true,
|
||||
isLoading = false,
|
||||
onChange,
|
||||
classNamePrefix,
|
||||
styles,
|
||||
}: ReactSelectDropdownProps) {
|
||||
const customStyles = useMemo(() => getCustomStyles<SelectOption>(), []);
|
||||
|
||||
@@ -46,8 +50,9 @@ export function ReactSelectDropdown({
|
||||
isSearchable={isSearchable}
|
||||
isLoading={isLoading}
|
||||
onChange={onChange}
|
||||
styles={customStyles}
|
||||
styles={styles || customStyles}
|
||||
className="w-full"
|
||||
classNamePrefix={classNamePrefix}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<p className="text-red-500 text-sm mt-1">{errorMessage}</p>
|
||||
|
||||
@@ -90,3 +90,26 @@ export const getCustomStyles = <T extends SelectOptionBase>(): StylesConfig<
|
||||
color: "#B7BDC2", // tertiary-light
|
||||
}),
|
||||
});
|
||||
|
||||
export const getGitProviderMicroagentManagementCustomStyles = <
|
||||
T extends SelectOptionBase,
|
||||
>(): StylesConfig<T, false> => ({
|
||||
...getCustomStyles<T>(),
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isDisabled ? "#363636" : "#454545", // darker tertiary when disabled
|
||||
border: "1px solid #717888",
|
||||
borderRadius: "0.125rem",
|
||||
minHeight: "2.5rem",
|
||||
padding: "0 0.5rem",
|
||||
boxShadow: "none",
|
||||
opacity: state.isDisabled ? 0.6 : 1,
|
||||
cursor: state.isDisabled ? "not-allowed" : "pointer",
|
||||
"&:hover": {
|
||||
borderColor: "#717888",
|
||||
},
|
||||
"& .git-provider-dropdown__value-container": {
|
||||
padding: "2px 0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -24,6 +24,17 @@ import { AgentState } from "#/types/agent-state";
|
||||
import { getFirstPRUrl } from "#/utils/parse-pr-url";
|
||||
import MemoryIcon from "#/icons/memory_icon.svg?react";
|
||||
|
||||
const isErrorEvent = (evt: unknown): evt is { error: true; message: string } =>
|
||||
typeof evt === "object" &&
|
||||
evt !== null &&
|
||||
"error" in evt &&
|
||||
evt.error === true;
|
||||
|
||||
const isAgentStatusError = (evt: unknown): boolean =>
|
||||
isOpenHandsEvent(evt) &&
|
||||
isAgentStateChangeObservation(evt) &&
|
||||
evt.extras.agent_state === AgentState.ERROR;
|
||||
|
||||
interface MessagesProps {
|
||||
messages: (OpenHandsAction | OpenHandsObservation)[];
|
||||
isAwaitingUserConfirmation: boolean;
|
||||
@@ -31,8 +42,11 @@ interface MessagesProps {
|
||||
|
||||
export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
({ messages, isAwaitingUserConfirmation }) => {
|
||||
const { createConversationAndSubscribe, isPending } =
|
||||
useCreateConversationAndSubscribeMultiple();
|
||||
const {
|
||||
createConversationAndSubscribe,
|
||||
isPending,
|
||||
unsubscribeFromConversation,
|
||||
} = useCreateConversationAndSubscribeMultiple();
|
||||
const { getOptimisticUserMessage } = useOptimisticUserMessage();
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: conversation } = useUserConversation(conversationId);
|
||||
@@ -93,20 +107,6 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
|
||||
const handleMicroagentEvent = React.useCallback(
|
||||
(socketEvent: unknown, microagentConversationId: string) => {
|
||||
// Handle error events
|
||||
const isErrorEvent = (
|
||||
evt: unknown,
|
||||
): evt is { error: true; message: string } =>
|
||||
typeof evt === "object" &&
|
||||
evt !== null &&
|
||||
"error" in evt &&
|
||||
evt.error === true;
|
||||
|
||||
const isAgentStatusError = (evt: unknown): boolean =>
|
||||
isOpenHandsEvent(evt) &&
|
||||
isAgentStateChangeObservation(evt) &&
|
||||
evt.extras.agent_state === AgentState.ERROR;
|
||||
|
||||
if (isErrorEvent(socketEvent) || isAgentStatusError(socketEvent)) {
|
||||
setMicroagentStatuses((prev) =>
|
||||
prev.map((statusEntry) =>
|
||||
@@ -119,7 +119,11 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
isOpenHandsEvent(socketEvent) &&
|
||||
isAgentStateChangeObservation(socketEvent)
|
||||
) {
|
||||
if (socketEvent.extras.agent_state === AgentState.FINISHED) {
|
||||
// Handle completion states
|
||||
if (
|
||||
socketEvent.extras.agent_state === AgentState.FINISHED ||
|
||||
socketEvent.extras.agent_state === AgentState.AWAITING_USER_INPUT
|
||||
) {
|
||||
setMicroagentStatuses((prev) =>
|
||||
prev.map((statusEntry) =>
|
||||
statusEntry.conversationId === microagentConversationId
|
||||
@@ -127,6 +131,8 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
: statusEntry,
|
||||
),
|
||||
);
|
||||
|
||||
unsubscribeFromConversation(microagentConversationId);
|
||||
}
|
||||
} else if (
|
||||
isOpenHandsEvent(socketEvent) &&
|
||||
@@ -147,9 +153,27 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribeFromConversation(microagentConversationId);
|
||||
} else {
|
||||
// For any other event, transition from WAITING to CREATING if still waiting
|
||||
setMicroagentStatuses((prev) => {
|
||||
const currentStatus = prev.find(
|
||||
(entry) => entry.conversationId === microagentConversationId,
|
||||
)?.status;
|
||||
|
||||
if (currentStatus === MicroagentStatus.WAITING) {
|
||||
return prev.map((statusEntry) =>
|
||||
statusEntry.conversationId === microagentConversationId
|
||||
? { ...statusEntry, status: MicroagentStatus.CREATING }
|
||||
: statusEntry,
|
||||
);
|
||||
}
|
||||
return prev; // No change needed
|
||||
});
|
||||
}
|
||||
},
|
||||
[setMicroagentStatuses],
|
||||
[setMicroagentStatuses, unsubscribeFromConversation],
|
||||
);
|
||||
|
||||
const handleLaunchMicroagent = (
|
||||
@@ -178,13 +202,13 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
},
|
||||
onSuccessCallback: (newConversationId: string) => {
|
||||
setShowLaunchMicroagentModal(false);
|
||||
// Update status with conversation ID
|
||||
// Update status with conversation ID - start with WAITING
|
||||
setMicroagentStatuses((prev) => [
|
||||
...prev.filter((status) => status.eventId !== selectedEventId),
|
||||
{
|
||||
eventId: selectedEventId,
|
||||
conversationId: newConversationId,
|
||||
status: MicroagentStatus.CREATING,
|
||||
status: MicroagentStatus.WAITING,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -19,6 +19,8 @@ export function MicroagentStatusIndicator({
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (status) {
|
||||
case MicroagentStatus.WAITING:
|
||||
return t("MICROAGENT$STATUS_WAITING");
|
||||
case MicroagentStatus.CREATING:
|
||||
return t("MICROAGENT$STATUS_CREATING");
|
||||
case MicroagentStatus.COMPLETED:
|
||||
@@ -35,6 +37,8 @@ export function MicroagentStatusIndicator({
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (status) {
|
||||
case MicroagentStatus.WAITING:
|
||||
return <Spinner size="sm" />;
|
||||
case MicroagentStatus.CREATING:
|
||||
return <Spinner size="sm" />;
|
||||
case MicroagentStatus.COMPLETED:
|
||||
|
||||
@@ -10,6 +10,11 @@ interface ConversationCreatedToastProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface ConversationStartingToastProps {
|
||||
conversationId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ConversationCreatedToast({
|
||||
conversationId,
|
||||
onClose,
|
||||
@@ -37,6 +42,33 @@ function ConversationCreatedToast({
|
||||
);
|
||||
}
|
||||
|
||||
function ConversationStartingToast({
|
||||
conversationId,
|
||||
onClose,
|
||||
}: ConversationStartingToastProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-start gap-2">
|
||||
<Spinner size="sm" />
|
||||
<div>
|
||||
{t("MICROAGENT$CONVERSATION_STARTING")}
|
||||
<br />
|
||||
<a
|
||||
href={`/conversations/${conversationId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{t("MICROAGENT$VIEW_CONVERSATION")}
|
||||
</a>
|
||||
</div>
|
||||
<button type="button" onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConversationFinishedToastProps {
|
||||
conversationId: string;
|
||||
onClose: () => void;
|
||||
@@ -78,10 +110,18 @@ function ConversationErroredToast({
|
||||
errorMessage,
|
||||
onClose,
|
||||
}: ConversationErroredToastProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Check if the error message is a translation key
|
||||
const displayMessage =
|
||||
errorMessage === "MICROAGENT$UNKNOWN_ERROR"
|
||||
? t(errorMessage)
|
||||
: errorMessage;
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2">
|
||||
<SuccessIndicator status="error" />
|
||||
<div>{errorMessage}</div>
|
||||
<div>{displayMessage}</div>
|
||||
<button type="button" onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
@@ -136,3 +176,18 @@ export const renderConversationErroredToast = (
|
||||
duration: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
export const renderConversationStartingToast = (conversationId: string) =>
|
||||
toast(
|
||||
(toastInstance) => (
|
||||
<ConversationStartingToast
|
||||
conversationId={conversationId}
|
||||
onClose={() => toast.dismiss(toastInstance.id)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
...TOAST_OPTIONS,
|
||||
id: `starting-${conversationId}`,
|
||||
duration: 10000, // Show for 10 seconds or until dismissed
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function MicroagentManagementAccordionTitle({
|
||||
<TooltipButton
|
||||
tooltip={repository.full_name}
|
||||
ariaLabel={repository.full_name}
|
||||
className="text-white text-base font-normal bg-transparent p-0 min-w-0 h-auto cursor-pointer truncate max-w-[200px] translate-y-[-1px]"
|
||||
className="text-white text-base font-normal bg-transparent p-0 min-w-0 h-auto cursor-pointer truncate max-w-[194px] translate-y-[-1px]"
|
||||
testId="repository-name-tooltip"
|
||||
placement="bottom"
|
||||
>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { getFirstPRUrl } from "#/utils/parse-pr-url";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
// Handle error events
|
||||
const isErrorEvent = (evt: unknown): evt is { error: true; message: string } =>
|
||||
@@ -65,16 +66,10 @@ const getConversationInstructions = (
|
||||
gitProvider: Provider,
|
||||
) => `Create a microagent for the repository ${repositoryName} by following the steps below:
|
||||
|
||||
- Step 1: Create a markdown file inside the .openhands/microagents folder with the name of the microagent (The microagent must be created in the .openhands/microagents folder and should be able to perform the described task when triggered).
|
||||
|
||||
- This is the instructions about what the microagent should do: ${formData.query}
|
||||
|
||||
${
|
||||
- Step 1: Create a markdown file inside the .openhands/microagents folder with the name of the microagent (The microagent must be created in the .openhands/microagents folder and should be able to perform the described task when triggered). This is the instructions about what the microagent should do: ${formData.query}. ${
|
||||
formData.triggers && formData.triggers.length > 0
|
||||
? `
|
||||
- This is the triggers of the microagent: ${formData.triggers.join(", ")}
|
||||
`
|
||||
: "- Please be noted that the microagent doesn't have any triggers."
|
||||
? `This is the triggers of the microagent: ${formData.triggers.join(", ")}`
|
||||
: "Please be noted that the microagent doesn't have any triggers."
|
||||
}
|
||||
|
||||
- Step 2: Create a new branch for the repository ${repositoryName}, must avoid duplicated branches.
|
||||
@@ -91,16 +86,10 @@ const getUpdateConversationInstructions = (
|
||||
) => `Update the microagent for the repository ${repositoryName} by following the steps below:
|
||||
|
||||
|
||||
- Step 1: Update the microagent. This is the path of the microagent: ${formData.microagentPath} (The updated microagent must be in the .openhands/microagents folder and should be able to perform the described task when triggered).
|
||||
|
||||
- This is the updated instructions about what the microagent should do: ${formData.query}
|
||||
|
||||
${
|
||||
- Step 1: Update the microagent. This is the path of the microagent: ${formData.microagentPath} (The updated microagent must be in the .openhands/microagents folder and should be able to perform the described task when triggered). This is the updated instructions about what the microagent should do: ${formData.query}. ${
|
||||
formData.triggers && formData.triggers.length > 0
|
||||
? `
|
||||
- This is the triggers of the microagent: ${formData.triggers.join(", ")}
|
||||
`
|
||||
: "- Please be noted that the microagent doesn't have any triggers."
|
||||
? `This is the triggers of the microagent: ${formData.triggers.join(", ")}`
|
||||
: "Please be noted that the microagent doesn't have any triggers."
|
||||
}
|
||||
|
||||
- Step 2: Create a new branch for the repository ${repositoryName}, must avoid duplicated branches.
|
||||
@@ -119,6 +108,8 @@ export function MicroagentManagementContent() {
|
||||
learnThisRepoModalVisible,
|
||||
} = useSelector((state: RootState) => state.microagentManagement);
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -182,11 +173,7 @@ export function MicroagentManagementContent() {
|
||||
// Check if agent has finished and we have a PR
|
||||
if (isOpenHandsEvent(socketEvent) && isFinishAction(socketEvent)) {
|
||||
const prUrl = getFirstPRUrl(socketEvent.args.final_thought || "");
|
||||
if (prUrl) {
|
||||
displaySuccessToast(
|
||||
t(I18nKey.MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW),
|
||||
);
|
||||
} else {
|
||||
if (!prUrl) {
|
||||
// Agent finished but no PR found
|
||||
displaySuccessToast(t(I18nKey.MICROAGENT_MANAGEMENT$PR_NOT_CREATED));
|
||||
}
|
||||
@@ -329,11 +316,18 @@ export function MicroagentManagementContent() {
|
||||
</>
|
||||
);
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
|
||||
if (width < 1024) {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-6">
|
||||
<div className="w-full rounded-lg border border-[#525252] bg-[#24272E] max-h-[494px] min-h-[494px]">
|
||||
<MicroagentManagementSidebar isSmallerScreen />
|
||||
{providersAreSet && (
|
||||
<MicroagentManagementSidebar
|
||||
isSmallerScreen
|
||||
providers={providers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full rounded-lg border border-[#525252] bg-[#24272E] flex-1 min-h-[494px]">
|
||||
<MicroagentManagementMain />
|
||||
@@ -345,7 +339,7 @@ export function MicroagentManagementContent() {
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex rounded-lg border border-[#525252] bg-[#24272E] overflow-hidden">
|
||||
<MicroagentManagementSidebar />
|
||||
{providersAreSet && <MicroagentManagementSidebar providers={providers} />}
|
||||
<div className="flex-1">
|
||||
<MicroagentManagementMain />
|
||||
</div>
|
||||
|
||||
@@ -59,8 +59,10 @@ export function MicroagentManagementMicroagentCard({
|
||||
if (runtimeStatus === "STATUS$ERROR") {
|
||||
return t(I18nKey.MICROAGENT$STATUS_ERROR);
|
||||
}
|
||||
if (conversationStatus === "RUNNING" && runtimeStatus === "STATUS$READY") {
|
||||
return t(I18nKey.MICROAGENT$STATUS_OPENING_PR);
|
||||
if (conversationStatus === "RUNNING") {
|
||||
return runtimeStatus === "STATUS$READY"
|
||||
? t(I18nKey.MICROAGENT$STATUS_OPENING_PR)
|
||||
: t(I18nKey.COMMON$STARTING);
|
||||
}
|
||||
return "";
|
||||
}, [conversationStatus, runtimeStatus, t, hasPr]);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Accordion, AccordionItem } from "@heroui/react";
|
||||
import { MicroagentManagementRepoMicroagents } from "./microagent-management-repo-microagents";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { TabType } from "#/types/microagent-management";
|
||||
import { MicroagentManagementNoRepositories } from "./microagent-management-no-repositories";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { DOCUMENTATION_URL } from "#/utils/constants";
|
||||
import { MicroagentManagementAccordionTitle } from "./microagent-management-accordion-title";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
|
||||
type MicroagentManagementRepositoriesProps = {
|
||||
repositories: GitRepository[];
|
||||
@@ -21,23 +18,9 @@ export function MicroagentManagementRepositories({
|
||||
tabType,
|
||||
}: MicroagentManagementRepositoriesProps) {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const numberOfRepoMicroagents = repositories.length;
|
||||
|
||||
// Filter repositories based on search query
|
||||
const filteredRepositories = useMemo(() => {
|
||||
if (!searchQuery.trim()) {
|
||||
return repositories;
|
||||
}
|
||||
|
||||
const sanitizedQuery = sanitizeQuery(searchQuery);
|
||||
return repositories.filter((repository) => {
|
||||
const sanitizedRepoName = sanitizeQuery(repository.full_name);
|
||||
return sanitizedRepoName.includes(sanitizedQuery);
|
||||
});
|
||||
}, [repositories, searchQuery]);
|
||||
|
||||
if (numberOfRepoMicroagents === 0) {
|
||||
if (tabType === "personal") {
|
||||
return (
|
||||
@@ -73,25 +56,6 @@ export function MicroagentManagementRepositories({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{/* Search Input */}
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<label htmlFor="repository-search" className="sr-only">
|
||||
{t(I18nKey.COMMON$SEARCH_REPOSITORIES)}
|
||||
</label>
|
||||
<input
|
||||
id="repository-search"
|
||||
name="repository-search"
|
||||
type="text"
|
||||
placeholder={`${t(I18nKey.COMMON$SEARCH_REPOSITORIES)}...`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={cn(
|
||||
"bg-tertiary border border-[#717888] bg-[#454545] w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Repositories Accordion */}
|
||||
<Accordion
|
||||
variant="splitted"
|
||||
@@ -104,7 +68,7 @@ export function MicroagentManagementRepositories({
|
||||
}}
|
||||
selectionMode="multiple"
|
||||
>
|
||||
{filteredRepositories.map((repository) => (
|
||||
{repositories.map((repository) => (
|
||||
<AccordionItem
|
||||
key={repository.id}
|
||||
aria-label={repository.full_name}
|
||||
|
||||
@@ -1,59 +1,109 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentManagementSidebarHeader } from "./microagent-management-sidebar-header";
|
||||
import { MicroagentManagementSidebarTabs } from "./microagent-management-sidebar-tabs";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { useGitRepositories } from "#/hooks/query/use-git-repositories";
|
||||
import { GitProviderDropdown } from "#/components/common/git-provider-dropdown";
|
||||
import {
|
||||
setPersonalRepositories,
|
||||
setOrganizationRepositories,
|
||||
setRepositories,
|
||||
} from "#/state/microagent-management-slice";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { getGitProviderMicroagentManagementCustomStyles } from "#/components/common/react-select-styles";
|
||||
|
||||
interface MicroagentManagementSidebarProps {
|
||||
isSmallerScreen?: boolean;
|
||||
providers: Provider[];
|
||||
}
|
||||
|
||||
export function MicroagentManagementSidebar({
|
||||
isSmallerScreen = false,
|
||||
providers,
|
||||
}: MicroagentManagementSidebarProps) {
|
||||
const [selectedProvider, setSelectedProvider] = useState<Provider | null>(
|
||||
providers.length > 0 ? providers[0] : null,
|
||||
);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { providers } = useUserProviders();
|
||||
const selectedProvider = providers.length > 0 ? providers[0] : null;
|
||||
const { data: repositories, isLoading } =
|
||||
useUserRepositories(selectedProvider);
|
||||
|
||||
const { data: repositories, isLoading } = useGitRepositories({
|
||||
provider: selectedProvider,
|
||||
pageSize: 200,
|
||||
enabled: !!selectedProvider,
|
||||
});
|
||||
|
||||
// Auto-select provider if there's only one
|
||||
useEffect(() => {
|
||||
if (providers.length > 0 && !selectedProvider) {
|
||||
setSelectedProvider(providers[0]);
|
||||
}
|
||||
}, [providers, selectedProvider]);
|
||||
|
||||
const handleProviderChange = (provider: Provider | null) => {
|
||||
setSelectedProvider(provider);
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
// Filter repositories based on search query
|
||||
const filteredRepositories = useMemo(() => {
|
||||
if (!repositories?.pages) return null;
|
||||
|
||||
// Flatten all pages to get all repositories
|
||||
const allRepositories = repositories.pages.flatMap((page) => page.data);
|
||||
|
||||
if (!searchQuery.trim()) {
|
||||
return allRepositories;
|
||||
}
|
||||
|
||||
const sanitizedQuery = sanitizeQuery(searchQuery);
|
||||
return allRepositories.filter((repository: GitRepository) => {
|
||||
const sanitizedRepoName = sanitizeQuery(repository.full_name);
|
||||
return sanitizedRepoName.includes(sanitizedQuery);
|
||||
});
|
||||
}, [repositories, searchQuery, selectedProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (repositories?.pages) {
|
||||
const personalRepos: GitRepository[] = [];
|
||||
const organizationRepos: GitRepository[] = [];
|
||||
const otherRepos: GitRepository[] = [];
|
||||
|
||||
// Flatten all pages to get all repositories
|
||||
const allRepositories = repositories.pages.flatMap((page) => page.data);
|
||||
|
||||
allRepositories.forEach((repo: GitRepository) => {
|
||||
const hasOpenHandsSuffix = repo.full_name.endsWith("/.openhands");
|
||||
|
||||
if (repo.owner_type === "user" && hasOpenHandsSuffix) {
|
||||
personalRepos.push(repo);
|
||||
} else if (repo.owner_type === "organization" && hasOpenHandsSuffix) {
|
||||
organizationRepos.push(repo);
|
||||
} else {
|
||||
otherRepos.push(repo);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(setPersonalRepositories(personalRepos));
|
||||
dispatch(setOrganizationRepositories(organizationRepos));
|
||||
dispatch(setRepositories(otherRepos));
|
||||
if (!filteredRepositories?.length) {
|
||||
dispatch(setPersonalRepositories([]));
|
||||
dispatch(setOrganizationRepositories([]));
|
||||
dispatch(setRepositories([]));
|
||||
return;
|
||||
}
|
||||
}, [repositories, dispatch]);
|
||||
|
||||
const personalRepos: GitRepository[] = [];
|
||||
const organizationRepos: GitRepository[] = [];
|
||||
const otherRepos: GitRepository[] = [];
|
||||
|
||||
filteredRepositories.forEach((repo: GitRepository) => {
|
||||
const hasOpenHandsSuffix =
|
||||
selectedProvider === "gitlab"
|
||||
? repo.full_name.endsWith("/openhands-config")
|
||||
: repo.full_name.endsWith("/.openhands");
|
||||
|
||||
if (repo.owner_type === "user" && hasOpenHandsSuffix) {
|
||||
personalRepos.push(repo);
|
||||
} else if (repo.owner_type === "organization" && hasOpenHandsSuffix) {
|
||||
organizationRepos.push(repo);
|
||||
} else {
|
||||
otherRepos.push(repo);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(setPersonalRepositories(personalRepos));
|
||||
dispatch(setOrganizationRepositories(organizationRepos));
|
||||
dispatch(setRepositories(otherRepos));
|
||||
}, [filteredRepositories, selectedProvider, dispatch]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -63,6 +113,41 @@ export function MicroagentManagementSidebar({
|
||||
)}
|
||||
>
|
||||
<MicroagentManagementSidebarHeader />
|
||||
|
||||
{/* Provider Selection */}
|
||||
{providers.length > 1 && (
|
||||
<div className="mt-6">
|
||||
<GitProviderDropdown
|
||||
providers={providers}
|
||||
value={selectedProvider}
|
||||
placeholder="Select Provider"
|
||||
onChange={handleProviderChange}
|
||||
className="w-full"
|
||||
classNamePrefix="git-provider-dropdown"
|
||||
styles={getGitProviderMicroagentManagementCustomStyles()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="flex flex-col gap-2 w-full mt-6">
|
||||
<label htmlFor="repository-search" className="sr-only">
|
||||
{t(I18nKey.COMMON$SEARCH_REPOSITORIES)}
|
||||
</label>
|
||||
<input
|
||||
id="repository-search"
|
||||
name="repository-search"
|
||||
type="text"
|
||||
placeholder={`${t(I18nKey.COMMON$SEARCH_REPOSITORIES)}...`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={cn(
|
||||
"bg-tertiary border border-[#717888] bg-[#454545] w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed h-10 box-shadow-none outline-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 flex-1">
|
||||
<Spinner size="sm" />
|
||||
|
||||
@@ -95,10 +95,10 @@ export function ConversationSubscriptionsProvider({
|
||||
[],
|
||||
);
|
||||
|
||||
const unsubscribeFromConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
// Get a local reference to the socket data to avoid race conditions
|
||||
const socketData = conversationSockets[conversationId];
|
||||
const unsubscribeFromConversation = useCallback((conversationId: string) => {
|
||||
// Use functional update to access current socket data and perform cleanup
|
||||
setConversationSockets((prev) => {
|
||||
const socketData = prev[conversationId];
|
||||
|
||||
if (socketData) {
|
||||
const { socket } = socketData;
|
||||
@@ -112,24 +112,23 @@ export function ConversationSubscriptionsProvider({
|
||||
socket.disconnect();
|
||||
}
|
||||
|
||||
// Update state to remove the socket
|
||||
setConversationSockets((prev) => {
|
||||
const newSockets = { ...prev };
|
||||
delete newSockets[conversationId];
|
||||
return newSockets;
|
||||
});
|
||||
|
||||
// Remove from active IDs
|
||||
setActiveConversationIds((prev) =>
|
||||
prev.filter((id) => id !== conversationId),
|
||||
);
|
||||
|
||||
// Clean up event handler reference
|
||||
delete eventHandlersRef.current[conversationId];
|
||||
|
||||
// Remove the socket from state
|
||||
const newSockets = { ...prev };
|
||||
delete newSockets[conversationId];
|
||||
return newSockets;
|
||||
}
|
||||
},
|
||||
[conversationSockets],
|
||||
);
|
||||
|
||||
return prev; // No change if socket not found
|
||||
});
|
||||
|
||||
// Remove from active IDs
|
||||
setActiveConversationIds((prev) =>
|
||||
prev.filter((id) => id !== conversationId),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const subscribeToConversation = useCallback(
|
||||
(options: {
|
||||
@@ -173,9 +172,7 @@ export function ConversationSubscriptionsProvider({
|
||||
if (isErrorEvent(event) || isAgentStatusError(event)) {
|
||||
renderConversationErroredToast(
|
||||
conversationId,
|
||||
isErrorEvent(event)
|
||||
? event.message
|
||||
: "Unknown error, please try again",
|
||||
isErrorEvent(event) ? event.message : "MICROAGENT$UNKNOWN_ERROR",
|
||||
);
|
||||
} else if (isStatusUpdate(event)) {
|
||||
if (event.type === "info" && event.id === "STATUS$STARTING_RUNTIME") {
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import React from "react";
|
||||
import { useQueries, type Query } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { AxiosError } from "axios";
|
||||
import { useCreateConversation } from "./mutation/use-create-conversation";
|
||||
import { useUserProviders } from "./use-user-providers";
|
||||
import { useConversationSubscriptions } from "#/context/conversation-subscriptions-provider";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { CreateMicroagent } from "#/api/open-hands.types";
|
||||
import { CreateMicroagent, Conversation } from "#/api/open-hands.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { renderConversationStartingToast } from "#/components/features/chat/microagent/microagent-status-toast";
|
||||
|
||||
interface ConversationData {
|
||||
conversationId: string;
|
||||
sessionApiKey: string | null;
|
||||
baseUrl: string;
|
||||
onEventCallback?: (event: unknown, conversationId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to create a conversation and subscribe to it, supporting multiple subscriptions.
|
||||
* This extends the functionality of useCreateConversationAndSubscribe to allow subscribing to
|
||||
* multiple conversations simultaneously.
|
||||
* This version waits for conversation status to be "RUNNING" before establishing WebSocket connection.
|
||||
* Shows immediate toast feedback and polls conversation status until ready.
|
||||
*/
|
||||
export const useCreateConversationAndSubscribeMultiple = () => {
|
||||
const { mutate: createConversation, isPending } = useCreateConversation();
|
||||
@@ -20,6 +32,87 @@ export const useCreateConversationAndSubscribeMultiple = () => {
|
||||
activeConversationIds,
|
||||
} = useConversationSubscriptions();
|
||||
|
||||
// Store conversation data immediately after creation
|
||||
const [createdConversations, setCreatedConversations] = React.useState<
|
||||
Record<string, ConversationData>
|
||||
>({});
|
||||
|
||||
// Get conversation IDs that need polling
|
||||
const conversationIdsToWatch = Object.keys(createdConversations);
|
||||
|
||||
// Poll each conversation until it's ready
|
||||
const conversationQueries = useQueries({
|
||||
queries: conversationIdsToWatch.map((conversationId) => ({
|
||||
queryKey: ["conversation-ready-poll", conversationId],
|
||||
queryFn: () => OpenHands.getConversation(conversationId),
|
||||
enabled: !!conversationId,
|
||||
refetchInterval: (query: Query<Conversation | null, AxiosError>) => {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "STARTING") {
|
||||
return 3000; // Poll every 3 seconds while STARTING
|
||||
}
|
||||
return false; // Stop polling once not STARTING
|
||||
},
|
||||
retry: false,
|
||||
})),
|
||||
});
|
||||
|
||||
// Extract stable values from queries for dependency array
|
||||
const queryStatuses = conversationQueries.map((query) => query.data?.status);
|
||||
const queryDataExists = conversationQueries.map((query) => !!query.data);
|
||||
|
||||
// Effect to handle subscription when conversations are ready
|
||||
React.useEffect(() => {
|
||||
conversationQueries.forEach((query, index) => {
|
||||
const conversationId = conversationIdsToWatch[index];
|
||||
const conversationData = createdConversations[conversationId];
|
||||
|
||||
if (!query.data || !conversationData) return;
|
||||
|
||||
const { status, url, session_api_key: sessionApiKey } = query.data;
|
||||
|
||||
let { baseUrl } = conversationData;
|
||||
if (url && !url.startsWith("/")) {
|
||||
baseUrl = new URL(url).host;
|
||||
}
|
||||
|
||||
if (status === "RUNNING") {
|
||||
// Conversation is ready - subscribe to WebSocket
|
||||
subscribeToConversation({
|
||||
conversationId,
|
||||
sessionApiKey,
|
||||
providersSet: providers,
|
||||
baseUrl,
|
||||
onEvent: conversationData.onEventCallback,
|
||||
});
|
||||
|
||||
// Remove from created conversations (cleanup)
|
||||
setCreatedConversations((prev) => {
|
||||
const newCreated = { ...prev };
|
||||
delete newCreated[conversationId];
|
||||
return newCreated;
|
||||
});
|
||||
} else if (status === "STOPPED") {
|
||||
// Dismiss the starting toast
|
||||
toast.dismiss(`starting-${conversationId}`);
|
||||
|
||||
// Remove from created conversations (cleanup)
|
||||
setCreatedConversations((prev) => {
|
||||
const newCreated = { ...prev };
|
||||
delete newCreated[conversationId];
|
||||
return newCreated;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [
|
||||
queryStatuses,
|
||||
queryDataExists,
|
||||
conversationIdsToWatch,
|
||||
createdConversations,
|
||||
subscribeToConversation,
|
||||
providers,
|
||||
]);
|
||||
|
||||
const createConversationAndSubscribe = React.useCallback(
|
||||
({
|
||||
query,
|
||||
@@ -49,6 +142,15 @@ export const useCreateConversationAndSubscribeMultiple = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
// Show immediate toast to let user know something is happening
|
||||
renderConversationStartingToast(data.conversation_id);
|
||||
|
||||
// Call the success callback immediately
|
||||
if (onSuccessCallback) {
|
||||
onSuccessCallback(data.conversation_id);
|
||||
}
|
||||
|
||||
// Only handle immediate post-creation tasks here
|
||||
let baseUrl = "";
|
||||
if (data?.url && !data.url.startsWith("/")) {
|
||||
baseUrl = new URL(data.url).host;
|
||||
@@ -58,24 +160,21 @@ export const useCreateConversationAndSubscribeMultiple = () => {
|
||||
window?.location.host;
|
||||
}
|
||||
|
||||
// Subscribe to the conversation
|
||||
subscribeToConversation({
|
||||
conversationId: data.conversation_id,
|
||||
sessionApiKey: data.session_api_key,
|
||||
providersSet: providers,
|
||||
baseUrl,
|
||||
onEvent: onEventCallback,
|
||||
});
|
||||
|
||||
// Call the success callback if provided
|
||||
if (onSuccessCallback) {
|
||||
onSuccessCallback(data.conversation_id);
|
||||
}
|
||||
// Store conversation data for polling and eventual subscription
|
||||
setCreatedConversations((prev) => ({
|
||||
...prev,
|
||||
[data.conversation_id]: {
|
||||
conversationId: data.conversation_id,
|
||||
sessionApiKey: data.session_api_key,
|
||||
baseUrl,
|
||||
onEventCallback,
|
||||
},
|
||||
}));
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
[createConversation, subscribeToConversation, providers],
|
||||
[createConversation],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -814,6 +814,9 @@ export enum I18nKey {
|
||||
MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW = "MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW",
|
||||
MICROAGENT_MANAGEMENT$PR_NOT_CREATED = "MICROAGENT_MANAGEMENT$PR_NOT_CREATED",
|
||||
MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT = "MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT",
|
||||
MICROAGENT$STATUS_WAITING = "MICROAGENT$STATUS_WAITING",
|
||||
MICROAGENT$UNKNOWN_ERROR = "MICROAGENT$UNKNOWN_ERROR",
|
||||
MICROAGENT$CONVERSATION_STARTING = "MICROAGENT$CONVERSATION_STARTING",
|
||||
SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT = "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT",
|
||||
SETTINGS$SECURITY_ANALYZER_NONE = "SETTINGS$SECURITY_ANALYZER_NONE",
|
||||
SETTINGS$SECURITY_ANALYZER_INVARIANT = "SETTINGS$SECURITY_ANALYZER_INVARIANT",
|
||||
|
||||
@@ -13023,6 +13023,54 @@
|
||||
"de": "Etwas ist schiefgelaufen. Versuchen Sie, den Microagenten erneut zu starten.",
|
||||
"uk": "Щось пішло не так. Спробуйте ініціювати мікроагента ще раз."
|
||||
},
|
||||
"MICROAGENT$STATUS_WAITING": {
|
||||
"en": "Waiting for runtime to start...",
|
||||
"ja": "ランタイムの開始を待機中...",
|
||||
"zh-CN": "等待运行时启动...",
|
||||
"zh-TW": "等待運行時啟動...",
|
||||
"ko-KR": "런타임 시작을 기다리는 중...",
|
||||
"no": "Venter på at runtime skal starte...",
|
||||
"it": "In attesa dell'avvio del runtime...",
|
||||
"pt": "Aguardando o runtime iniciar...",
|
||||
"es": "Esperando que inicie el runtime...",
|
||||
"ar": "في انتظار بدء وقت التشغيل...",
|
||||
"fr": "En attente du démarrage du runtime...",
|
||||
"tr": "Çalışma zamanının başlaması bekleniyor...",
|
||||
"de": "Warten auf den Start der Laufzeit...",
|
||||
"uk": "Очікування запуску середовища виконання..."
|
||||
},
|
||||
"MICROAGENT$UNKNOWN_ERROR": {
|
||||
"en": "Unknown error, please try again",
|
||||
"ja": "不明なエラーです。もう一度お試しください",
|
||||
"zh-CN": "未知错误,请重试",
|
||||
"zh-TW": "未知錯誤,請重試",
|
||||
"ko-KR": "알 수 없는 오류입니다. 다시 시도해 주세요",
|
||||
"no": "Ukjent feil, vennligst prøv igjen",
|
||||
"it": "Errore sconosciuto, riprova",
|
||||
"pt": "Erro desconhecido, tente novamente",
|
||||
"es": "Error desconocido, inténtalo de nuevo",
|
||||
"ar": "خطأ غير معروف، يرجى المحاولة مرة أخرى",
|
||||
"fr": "Erreur inconnue, veuillez réessayer",
|
||||
"tr": "Bilinmeyen hata, lütfen tekrar deneyin",
|
||||
"de": "Unbekannter Fehler, bitte versuchen Sie es erneut",
|
||||
"uk": "Невідома помилка, спробуйте ще раз"
|
||||
},
|
||||
"MICROAGENT$CONVERSATION_STARTING": {
|
||||
"en": "Starting conversation...",
|
||||
"ja": "会話を開始しています...",
|
||||
"zh-CN": "正在开始对话...",
|
||||
"zh-TW": "正在開始對話...",
|
||||
"ko-KR": "대화를 시작하는 중...",
|
||||
"no": "Starter samtale...",
|
||||
"it": "Avvio conversazione...",
|
||||
"pt": "Iniciando conversa...",
|
||||
"es": "Iniciando conversación...",
|
||||
"ar": "بدء المحادثة...",
|
||||
"fr": "Démarrage de la conversation...",
|
||||
"tr": "Konuşma başlatılıyor...",
|
||||
"de": "Gespräch wird gestartet...",
|
||||
"uk": "Розпочинається розмова..."
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT": {
|
||||
"en": "LLM Analyzer (Default)",
|
||||
"ja": "LLMアナライザー(デフォルト)",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum MicroagentStatus {
|
||||
WAITING = "waiting",
|
||||
CREATING = "creating",
|
||||
COMPLETED = "completed",
|
||||
ERROR = "error",
|
||||
|
||||
@@ -24,6 +24,7 @@ from openhands.experiments.experiment_manager import ExperimentManagerImpl
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.constants import ROOM_KEY
|
||||
from openhands.server.conversation_manager.conversation_manager import (
|
||||
@@ -137,9 +138,11 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
user_id=user_id,
|
||||
session_api_key=session_api_key,
|
||||
),
|
||||
status=ConversationStatus.STARTING
|
||||
if sid in self._starting_conversation_ids
|
||||
else ConversationStatus.RUNNING,
|
||||
status=(
|
||||
ConversationStatus.STARTING
|
||||
if sid in self._starting_conversation_ids
|
||||
else ConversationStatus.RUNNING
|
||||
),
|
||||
)
|
||||
|
||||
async def _start_agent_loop(
|
||||
@@ -248,7 +251,11 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
response.raise_for_status()
|
||||
|
||||
init_conversation: dict[str, Any] = {
|
||||
'initial_user_msg': initial_user_msg,
|
||||
'initial_user_msg': (
|
||||
initial_user_msg.content
|
||||
if initial_user_msg and initial_user_msg.content
|
||||
else None
|
||||
),
|
||||
'image_urls': [],
|
||||
'replay_json': replay_json,
|
||||
'conversation_id': sid,
|
||||
@@ -335,6 +342,45 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
)
|
||||
container.stop()
|
||||
|
||||
async def _get_runtime_status_from_nested_runtime(
|
||||
self, conversation_id: str, nested_url: str
|
||||
) -> RuntimeStatus | None:
|
||||
"""Get runtime status from the nested runtime via API call.
|
||||
|
||||
Args:
|
||||
conversation_id: The conversation ID to query
|
||||
nested_url: The base URL of the nested runtime
|
||||
|
||||
Returns:
|
||||
The runtime status if available, None otherwise
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
headers={
|
||||
'X-Session-API-Key': self._get_session_api_key_for_conversation(
|
||||
conversation_id
|
||||
)
|
||||
}
|
||||
) as client:
|
||||
# Query the nested runtime for conversation info
|
||||
response = await client.get(nested_url)
|
||||
if response.status_code == 200:
|
||||
conversation_data = response.json()
|
||||
runtime_status_str = conversation_data.get('runtime_status')
|
||||
if runtime_status_str:
|
||||
# Convert string back to RuntimeStatus enum
|
||||
return RuntimeStatus(runtime_status_str)
|
||||
else:
|
||||
logger.debug(
|
||||
f'Failed to get conversation info for {conversation_id}: {response.status_code}'
|
||||
)
|
||||
except ValueError:
|
||||
logger.debug(f'Invalid runtime status value: {runtime_status_str}')
|
||||
except Exception as e:
|
||||
logger.debug(f'Could not get runtime status for {conversation_id}: {e}')
|
||||
|
||||
return None
|
||||
|
||||
async def get_agent_loop_info(
|
||||
self, user_id: str | None = None, filter_to_sids: set[str] | None = None
|
||||
) -> list[AgentLoopInfo]:
|
||||
@@ -355,6 +401,12 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
self.config.sandbox.local_runtime_url,
|
||||
os.getenv('NESTED_RUNTIME_BROWSER_HOST', ''),
|
||||
)
|
||||
|
||||
# Get runtime status from nested runtime
|
||||
runtime_status = await self._get_runtime_status_from_nested_runtime(
|
||||
conversation_id, nested_url
|
||||
)
|
||||
|
||||
agent_loop_info = AgentLoopInfo(
|
||||
conversation_id=conversation_id,
|
||||
url=nested_url,
|
||||
@@ -366,9 +418,12 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
sid=conversation_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
status=ConversationStatus.STARTING
|
||||
if conversation_id in self._starting_conversation_ids
|
||||
else ConversationStatus.RUNNING,
|
||||
status=(
|
||||
ConversationStatus.STARTING
|
||||
if conversation_id in self._starting_conversation_ids
|
||||
else ConversationStatus.RUNNING
|
||||
),
|
||||
runtime_status=runtime_status,
|
||||
)
|
||||
results.append(agent_loop_info)
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user