feat(frontend): integrate with the updated get microagents API for the microagent management page. (#10010)

This commit is contained in:
Hiep Le 2025-07-31 21:42:07 +07:00 committed by GitHub
parent b28e0533e0
commit 953902dcce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 223 additions and 161 deletions

View File

@ -105,22 +105,12 @@ describe("MicroagentManagement", () => {
const mockMicroagents: RepositoryMicroagent[] = [
{
name: "test-microagent-1",
type: "repo",
content: "Test microagent content 1",
triggers: ["test", "microagent"],
inputs: [],
tools: [],
created_at: "2021-10-01T12:00:00Z",
git_provider: "github",
path: ".openhands/microagents/test-microagent-1",
},
{
name: "test-microagent-2",
type: "knowledge",
content: "Test microagent content 2",
triggers: ["knowledge", "test"],
inputs: [],
tools: [],
created_at: "2021-10-02T12:00:00Z",
git_provider: "github",
path: ".openhands/microagents/test-microagent-2",
@ -173,6 +163,13 @@ describe("MicroagentManagement", () => {
vi.spyOn(OpenHands, "searchConversations").mockResolvedValue([
...mockConversations,
]);
// Setup default mock for getRepositoryMicroagentContent
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
content: "Original microagent content for testing updates",
path: ".openhands/microagents/update-test-microagent",
git_provider: "github",
triggers: ["test", "update"],
});
});
it("should render the microagent management page", async () => {
@ -1187,17 +1184,6 @@ describe("MicroagentManagement", () => {
expect(conversation1).toBeInTheDocument();
expect(conversation2).toBeInTheDocument();
// Check that created dates are displayed for conversations (there are multiple elements with the same text)
const createdDates = screen.getAllByText(
/COMMON\$CREATED_ON.*10\/01\/2021/,
);
expect(createdDates.length).toBeGreaterThan(0);
const createdDates2 = screen.getAllByText(
/COMMON\$CREATED_ON.*10\/02\/2021/,
);
expect(createdDates2.length).toBeGreaterThan(0);
});
it("should handle multiple repository expansions with conversations", async () => {
@ -1475,11 +1461,6 @@ describe("MicroagentManagement", () => {
describe("MicroagentManagementMain", () => {
const mockRepositoryMicroagent: RepositoryMicroagent = {
name: "test-microagent",
type: "repo",
content: "Test microagent content",
triggers: ["test", "microagent"],
inputs: [],
tools: [],
created_at: "2021-10-01T12:00:00Z",
git_provider: "github",
path: ".openhands/microagents/test-microagent",
@ -1820,11 +1801,6 @@ describe("MicroagentManagement", () => {
it("should handle microagent with all required properties", async () => {
const completeMicroagent: RepositoryMicroagent = {
name: "complete-microagent",
type: "knowledge",
content: "Complete microagent content with all properties",
triggers: ["complete", "test"],
inputs: ["input1", "input2"],
tools: ["tool1", "tool2"],
created_at: "2021-10-01T12:00:00Z",
git_provider: "github",
path: ".openhands/microagents/complete-microagent",
@ -1874,11 +1850,6 @@ describe("MicroagentManagement", () => {
describe("Update microagent functionality", () => {
const mockMicroagentForUpdate: RepositoryMicroagent = {
name: "update-test-microagent",
type: "repo",
content: "Original microagent content for testing updates",
triggers: ["original", "test"],
inputs: [],
tools: [],
created_at: "2021-10-01T12:00:00Z",
git_provider: "github",
path: ".openhands/microagents/update-test-microagent",
@ -1999,11 +1970,13 @@ describe("MicroagentManagement", () => {
},
});
// Check that the form fields are populated with existing data
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue(
"Original microagent content for testing updates",
);
// Wait for the content to be loaded and form fields to be populated
await waitFor(() => {
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue(
"Original microagent content for testing updates",
);
});
});
it("should handle update microagent form submission", async () => {
@ -2207,12 +2180,16 @@ describe("MicroagentManagement", () => {
it("should handle update modal with microagent that has no content", async () => {
const user = userEvent.setup();
const microagentWithoutContent = {
...mockMicroagentForUpdate,
content: "",
};
// Render with update modal visible and microagent without content
// Mock the content API to return empty content for this test
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
content: "",
path: ".openhands/microagents/update-test-microagent",
git_provider: "github",
triggers: [],
});
// Render with update modal visible and microagent
renderWithProviders(<RouterStub />, {
preloadedState: {
metrics: {
@ -2222,7 +2199,7 @@ describe("MicroagentManagement", () => {
},
microagentManagement: {
selectedMicroagentItem: {
microagent: microagentWithoutContent,
microagent: mockMicroagentForUpdate,
conversation: undefined,
},
addMicroagentModalVisible: false,
@ -2243,19 +2220,25 @@ describe("MicroagentManagement", () => {
},
});
// Check that the form field is empty
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue("");
// Wait for the content to be loaded and check that the form field is empty
await waitFor(() => {
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue("");
});
});
it("should handle update modal with microagent that has no triggers", async () => {
const user = userEvent.setup();
const microagentWithoutTriggers = {
...mockMicroagentForUpdate,
triggers: [],
};
// Render with update modal visible and microagent without triggers
// Mock the content API to return content without triggers for this test
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
content: "Original microagent content for testing updates",
path: ".openhands/microagents/update-test-microagent",
git_provider: "github",
triggers: [],
});
// Render with update modal visible and microagent
renderWithProviders(<RouterStub />, {
preloadedState: {
metrics: {
@ -2265,7 +2248,7 @@ describe("MicroagentManagement", () => {
},
microagentManagement: {
selectedMicroagentItem: {
microagent: microagentWithoutTriggers,
microagent: mockMicroagentForUpdate,
conversation: undefined,
},
addMicroagentModalVisible: false,
@ -2397,11 +2380,6 @@ describe("MicroagentManagement", () => {
getRepositoryMicroagentsSpy.mockResolvedValue([
{
name: "test-microagent",
type: "repo",
content: "Test content",
triggers: [],
inputs: [],
tools: [],
created_at: "2021-10-01",
git_provider: "github",
path: ".openhands/microagents/test",
@ -2486,11 +2464,6 @@ describe("MicroagentManagement", () => {
describe("Learn something new button functionality", () => {
const mockMicroagentForLearn: RepositoryMicroagent = {
name: "learn-test-microagent",
type: "repo",
content: "Test microagent content for learn functionality",
triggers: ["learn", "test"],
inputs: [],
tools: [],
created_at: "2021-10-01T12:00:00Z",
git_provider: "github",
path: ".openhands/microagents/learn-test-microagent",
@ -2586,6 +2559,14 @@ describe("MicroagentManagement", () => {
it("should populate form fields with current microagent data when learn button is clicked", async () => {
const user = userEvent.setup();
// Mock the content API to return the expected content for this test
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
content: "Test microagent content for learn functionality",
path: ".openhands/microagents/learn-test-microagent",
git_provider: "github",
triggers: ["learn", "test"],
});
// Render with selected microagent
renderWithProviders(<RouterStub />, {
preloadedState: {
@ -2626,21 +2607,27 @@ describe("MicroagentManagement", () => {
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
});
// Check that the form fields are populated with current microagent data
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue(
"Test microagent content for learn functionality",
);
// Wait for the content to be loaded and form to be populated
await waitFor(() => {
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue(
"Test microagent content for learn functionality",
);
});
});
it("should handle learn button click with microagent that has no content", async () => {
const user = userEvent.setup();
const microagentWithoutContent = {
...mockMicroagentForLearn,
content: "",
};
// Render with selected microagent without content
// Mock the content API to return empty content for this test
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
content: "",
path: ".openhands/microagents/learn-test-microagent",
git_provider: "github",
triggers: [],
});
// Render with selected microagent
renderWithProviders(<RouterStub />, {
preloadedState: {
metrics: {
@ -2650,7 +2637,7 @@ describe("MicroagentManagement", () => {
},
microagentManagement: {
selectedMicroagentItem: {
microagent: microagentWithoutContent,
microagent: mockMicroagentForLearn,
conversation: undefined,
},
addMicroagentModalVisible: false,
@ -2680,19 +2667,25 @@ describe("MicroagentManagement", () => {
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
});
// Check that the form field is empty
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue("");
// Wait for the content to be loaded and check that the form field is empty
await waitFor(() => {
const queryInput = screen.getByTestId("query-input");
expect(queryInput).toHaveValue("");
});
});
it("should handle learn button click with microagent that has no triggers", async () => {
const user = userEvent.setup();
const microagentWithoutTriggers = {
...mockMicroagentForLearn,
triggers: [],
};
// Render with selected microagent without triggers
// Mock the content API to return content without triggers for this test
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
content: "Test microagent content for learn functionality",
path: ".openhands/microagents/learn-test-microagent",
git_provider: "github",
triggers: [],
});
// Render with selected microagent
renderWithProviders(<RouterStub />, {
preloadedState: {
metrics: {
@ -2702,7 +2695,7 @@ describe("MicroagentManagement", () => {
},
microagentManagement: {
selectedMicroagentItem: {
microagent: microagentWithoutTriggers,
microagent: mockMicroagentForLearn,
conversation: undefined,
},
addMicroagentModalVisible: false,

View File

@ -14,6 +14,7 @@ import {
GetMicroagentsResponse,
GetMicroagentPromptResponse,
CreateMicroagent,
MicroagentContentResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
@ -524,7 +525,7 @@ class OpenHands {
}
/**
* Get the available microagents for a specific repository
* Get the available microagents for a repository
* @param owner The repository owner
* @param repo The repository name
* @returns The available microagents for the repository
@ -539,6 +540,27 @@ class OpenHands {
return data;
}
/**
* 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 getRepositoryMicroagentContent(
owner: string,
repo: string,
filePath: string,
): Promise<MicroagentContentResponse> {
const { data } = await openHands.get<MicroagentContentResponse>(
`/api/user/repository/${owner}/${repo}/microagents/content`,
{
params: { file_path: filePath },
},
);
return data;
}
static async getMicroagentPrompt(
conversationId: string,
eventId: number,

View File

@ -147,3 +147,10 @@ export interface CreateMicroagent {
git_provider?: Provider;
title?: string;
}
export interface MicroagentContentResponse {
content: string;
path: string;
git_provider: Provider;
triggers: string[];
}

View File

@ -2,7 +2,6 @@ import { useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { formatDateMMDDYYYY } from "#/utils/format-time-delta";
import { RepositoryMicroagent } from "#/types/microagent-management";
import { Conversation } from "#/api/open-hands.types";
import {
@ -38,22 +37,6 @@ export function MicroagentManagementMicroagentCard({
pr_number: prNumber,
} = conversation ?? {};
// Format the repository URL to point to the microagent file
const microagentFilePath = microagent
? `.openhands/microagents/${microagent.name}`
: "";
// Format the createdAt date using MM/DD/YYYY format
const formattedCreatedAt = useMemo(() => {
if (microagent) {
return formatDateMMDDYYYY(new Date(microagent.created_at));
}
if (conversation) {
return formatDateMMDDYYYY(new Date(conversation.created_at));
}
return "";
}, [microagent, conversation]);
const hasPr = !!(prNumber && prNumber.length > 0);
// Helper function to get status text
@ -131,12 +114,9 @@ export function MicroagentManagementMicroagentCard({
<div className="text-white text-[16px] font-semibold">{cardTitle}</div>
{!!microagent && (
<div className="text-white text-sm font-normal">
{microagentFilePath}
{microagent.path}
</div>
)}
<div className="text-white text-sm font-normal">
{t(I18nKey.COMMON$CREATED_ON)} {formattedCreatedAt}
</div>
</div>
</div>
);

View File

@ -8,11 +8,12 @@ import { BrandButton } from "../settings/brand-button";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import XIcon from "#/icons/x.svg?react";
import { cn } from "#/utils/utils";
import { cn, extractRepositoryInfo } from "#/utils/utils";
import { BadgeInput } from "#/components/shared/inputs/badge-input";
import { MicroagentFormData } from "#/types/microagent-management";
import { Branch, GitRepository } from "#/types/git";
import { useRepositoryBranches } from "#/hooks/query/use-repository-branches";
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
import {
BranchDropdown,
BranchLoadingState,
@ -51,13 +52,23 @@ export function MicroagentManagementUpsertMicroagentModal({
// Add a ref to track if the branch was manually cleared by the user
const branchManuallyClearedRef = useRef<boolean>(false);
// Extract owner and repo from full_name for content API
const { owner, repo, filePath } = extractRepositoryInfo(
selectedRepository,
microagent,
);
// Fetch microagent content when updating
const { data: microagentContentData, isLoading: isLoadingContent } =
useRepositoryMicroagentContent(owner, repo, filePath, true);
// Populate form fields with existing microagent data when updating
useEffect(() => {
if (isUpdate && microagent) {
setQuery(microagent.content);
setTriggers(microagent.triggers || []);
if (isUpdate && microagentContentData) {
setQuery(microagentContentData.content);
setTriggers(microagentContentData.triggers || []);
}
}, [isUpdate, microagent]);
}, [isUpdate, microagentContentData]);
const {
data: branches,
@ -294,10 +305,11 @@ export function MicroagentManagementUpsertMicroagentModal({
isLoading ||
isLoadingBranches ||
!selectedBranch ||
isBranchesError
isBranchesError ||
(isUpdate && isLoadingContent) // Disable while loading content for updates
}
>
{isLoading || isLoadingBranches
{isLoading || isLoadingBranches || (isUpdate && isLoadingContent)
? t(I18nKey.HOME$LOADING)
: t(I18nKey.MICROAGENT$LAUNCH)}
</BrandButton>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Spinner } from "@heroui/react";
import { useSelector } from "react-redux";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
@ -7,8 +9,12 @@ import { ul, ol } from "../markdown/list";
import { paragraph } from "../markdown/paragraph";
import { anchor } from "../markdown/anchor";
import { RootState } from "#/store";
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
import { I18nKey } from "#/i18n/declaration";
import { extractRepositoryInfo } from "#/utils/utils";
export function MicroagentManagementViewMicroagentContent() {
const { t } = useTranslation();
const { selectedMicroagentItem } = useSelector(
(state: RootState) => state.microagentManagement,
);
@ -19,55 +25,49 @@ export function MicroagentManagementViewMicroagentContent() {
const { microagent } = selectedMicroagentItem ?? {};
const transformMicroagentContent = (): string => {
if (!microagent) {
return "";
}
// Extract owner and repo from full_name (e.g., "owner/repo")
const { owner, repo, filePath } = extractRepositoryInfo(
selectedRepository,
microagent,
);
// If no triggers exist, return the content as-is
if (!microagent.triggers || microagent.triggers.length === 0) {
return microagent.content;
}
// Create the triggers frontmatter
const triggersFrontmatter = `
---
triggers:
${microagent.triggers.map((trigger) => ` - ${trigger}`).join("\n")}
---
`;
// Prepend the frontmatter to the content
return `
${triggersFrontmatter}
${microagent.content}
`;
};
// Fetch microagent content using the new API
const {
data: microagentData,
isLoading,
error,
} = useRepositoryMicroagentContent(owner, repo, filePath, true);
if (!microagent || !selectedRepository) {
return null;
}
// Transform the content to include triggers frontmatter if applicable
const transformedContent = transformMicroagentContent();
return (
<div className="w-full h-full p-6 bg-[#ffffff1a] rounded-2xl text-white text-sm">
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{transformedContent}
</Markdown>
{isLoading && (
<div className="flex items-center justify-center w-full h-full">
<Spinner size="lg" data-testid="loading-microagent-content-spinner" />
</div>
)}
{error && (
<div className="flex items-center justify-center w-full h-full">
{t(I18nKey.MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT)}
</div>
)}
{microagentData && !isLoading && !error && (
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{microagentData.content}
</Markdown>
)}
</div>
);
}

View File

@ -0,0 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
export const useRepositoryMicroagentContent = (
owner: string,
repo: string,
filePath: string,
cacheDisabled: boolean = false,
) =>
useQuery({
queryKey: ["repository", "microagent", "content", owner, repo, filePath],
queryFn: () =>
OpenHands.getRepositoryMicroagentContent(owner, repo, filePath),
enabled: !!owner && !!repo && !!filePath,
staleTime: cacheDisabled ? 0 : 1000 * 60 * 5, // 5 minutes
gcTime: cacheDisabled ? 0 : 1000 * 60 * 15, // 15 minutes
});

View File

@ -734,4 +734,5 @@ export enum I18nKey {
MICROAGENT_MANAGEMENT$WHAT_YOU_WOULD_LIKE_TO_KNOW_ABOUT_THIS_REPO = "MICROAGENT_MANAGEMENT$WHAT_YOU_WOULD_LIKE_TO_KNOW_ABOUT_THIS_REPO",
MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_KNOW_ABOUT_THIS_REPO = "MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_KNOW_ABOUT_THIS_REPO",
MICROAGENT_MANAGEMENT$UPDATE_MICROAGENT_MODAL_DESCRIPTION = "MICROAGENT_MANAGEMENT$UPDATE_MICROAGENT_MODAL_DESCRIPTION",
MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT = "MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT",
}

View File

@ -11742,5 +11742,21 @@
"tr": "OpenHands, talimatlarınıza göre mikro ajanı güncelleyecektir.",
"de": "OpenHands aktualisiert den Microagenten basierend auf Ihren Anweisungen.",
"uk": "OpenHands оновить мікроагента відповідно до ваших інструкцій."
},
"MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT": {
"en": "Error loading microagent content.",
"ja": "マイクロエージェントのコンテンツの読み込み中にエラーが発生しました。",
"zh-CN": "加载微代理内容时出错。",
"zh-TW": "載入微代理內容時發生錯誤。",
"ko-KR": "마이크로에이전트 콘텐츠를 불러오는 중 오류가 발생했습니다.",
"no": "Feil ved lasting av mikroagent-innhold.",
"it": "Errore durante il caricamento del contenuto del microagente.",
"pt": "Erro ao carregar o conteúdo do microagente.",
"es": "Error al cargar el contenido del microagente.",
"ar": "حدث خطأ أثناء تحميل محتوى الوكيل الدقيق.",
"fr": "Erreur lors du chargement du contenu du microagent.",
"tr": "Mikro ajan içeriği yüklenirken hata oluştu.",
"de": "Fehler beim Laden des Microagent-Inhalts.",
"uk": "Помилка під час завантаження вмісту мікроагента."
}
}

View File

@ -4,11 +4,6 @@ export type TabType = "personal" | "repositories" | "organizations";
export interface RepositoryMicroagent {
name: string;
type: "repo" | "knowledge";
content: string;
triggers: string[];
inputs: string[];
tools: string[];
created_at: string;
git_provider: string;
path: string;

View File

@ -207,3 +207,22 @@ export const constructMicroagentUrl = (
return "";
}
};
/**
* Extract repository owner, repo name, and file path from repository and microagent data
* @param selectedRepository The selected repository object with full_name property
* @param microagent The microagent object with path property
* @returns Object containing owner, repo, and filePath
*
* @example
* const { owner, repo, filePath } = extractRepositoryInfo(selectedRepository, microagent);
*/
export const extractRepositoryInfo = (
selectedRepository: { full_name?: string } | null | undefined,
microagent: { path?: string } | null | undefined,
) => {
const [owner, repo] = selectedRepository?.full_name?.split("/") || [];
const filePath = microagent?.path || "";
return { owner, repo, filePath };
};