From 9ef11bf9302c4b3edb257e6ecfb0f41f70ad5bec Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:25:10 +0700 Subject: [PATCH] feat: show available skills for v1 conversations (#12039) --- .openhands/microagents/repo.md | 2 +- .../conversation/conversation-name.test.tsx | 24 +- .../microagents/microagent-modal.test.tsx | 91 ---- .../modals/skills/skill-modal.test.tsx | 394 ++++++++++++++ .../v1-conversation-service.api.ts | 13 + .../v1-conversation-service.types.ts | 11 + .../features/controls/tools-context-menu.tsx | 30 +- .../components/features/controls/tools.tsx | 16 +- .../conversation-card-context-menu.tsx | 147 ----- .../conversation-card-context-menu.tsx | 12 +- ...croagent-content.tsx => skill-content.tsx} | 8 +- .../{microagent-item.tsx => skill-item.tsx} | 26 +- ...oagent-triggers.tsx => skill-triggers.tsx} | 6 +- ...empty-state.tsx => skills-empty-state.tsx} | 8 +- ...ing-state.tsx => skills-loading-state.tsx} | 2 +- ...dal-header.tsx => skills-modal-header.tsx} | 10 +- ...microagents-modal.tsx => skills-modal.tsx} | 74 ++- .../conversation-name-context-menu.tsx | 15 +- .../conversation/conversation-name.tsx | 20 +- ...roagents.ts => use-conversation-skills.ts} | 14 +- .../use-conversation-name-context-menu.ts | 17 +- frontend/src/i18n/declaration.ts | 15 +- frontend/src/i18n/translation.json | 172 +++--- .../app_conversation_models.py | 10 + .../app_conversation_router.py | 152 +++++- .../app_conversation_service_base.py | 6 +- .../test_app_conversation_service_base.py | 16 +- .../test_app_conversation_skills_endpoint.py | 503 ++++++++++++++++++ 28 files changed, 1325 insertions(+), 489 deletions(-) delete mode 100644 frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx create mode 100644 frontend/__tests__/components/modals/skills/skill-modal.test.tsx delete mode 100644 frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx rename frontend/src/components/features/conversation-panel/{microagent-content.tsx => skill-content.tsx} (76%) rename frontend/src/components/features/conversation-panel/{microagent-item.tsx => skill-item.tsx} (65%) rename frontend/src/components/features/conversation-panel/{microagent-triggers.tsx => skill-triggers.tsx} (81%) rename frontend/src/components/features/conversation-panel/{microagents-empty-state.tsx => skills-empty-state.tsx} (63%) rename frontend/src/components/features/conversation-panel/{microagents-loading-state.tsx => skills-loading-state.tsx} (80%) rename frontend/src/components/features/conversation-panel/{microagents-modal-header.tsx => skills-modal-header.tsx} (82%) rename frontend/src/components/features/conversation-panel/{microagents-modal.tsx => skills-modal.tsx} (50%) rename frontend/src/hooks/query/{use-conversation-microagents.ts => use-conversation-skills.ts} (62%) create mode 100644 tests/unit/app_server/test_app_conversation_skills_endpoint.py diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index ceb87bc2f7..cd3ef33074 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -63,7 +63,7 @@ Frontend: - We use TanStack Query (fka React Query) for data fetching and cache management - Data Access Layer: API client methods are located in `frontend/src/api` and should never be called directly from UI components - they must always be wrapped with TanStack Query - Custom hooks are located in `frontend/src/hooks/query/` and `frontend/src/hooks/mutation/` - - Query hooks should follow the pattern use[Resource] (e.g., `useConversationMicroagents`) + - Query hooks should follow the pattern use[Resource] (e.g., `useConversationSkills`) - Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`) - Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints diff --git a/frontend/__tests__/components/features/conversation/conversation-name.test.tsx b/frontend/__tests__/components/features/conversation/conversation-name.test.tsx index 572ca590b1..41078b69cb 100644 --- a/frontend/__tests__/components/features/conversation/conversation-name.test.tsx +++ b/frontend/__tests__/components/features/conversation/conversation-name.test.tsx @@ -42,7 +42,7 @@ vi.mock("react-i18next", async () => { BUTTON$EXPORT_CONVERSATION: "Export Conversation", BUTTON$DOWNLOAD_VIA_VSCODE: "Download via VS Code", BUTTON$SHOW_AGENT_TOOLS_AND_METADATA: "Show Agent Tools", - CONVERSATION$SHOW_MICROAGENTS: "Show Microagents", + CONVERSATION$SHOW_SKILLS: "Show Skills", BUTTON$DISPLAY_COST: "Display Cost", COMMON$CLOSE_CONVERSATION_STOP_RUNTIME: "Close Conversation (Stop Runtime)", @@ -290,7 +290,7 @@ describe("ConversationNameContextMenu", () => { onStop: vi.fn(), onDisplayCost: vi.fn(), onShowAgentTools: vi.fn(), - onShowMicroagents: vi.fn(), + onShowSkills: vi.fn(), onExportConversation: vi.fn(), onDownloadViaVSCode: vi.fn(), }; @@ -304,7 +304,7 @@ describe("ConversationNameContextMenu", () => { expect(screen.getByTestId("stop-button")).toBeInTheDocument(); expect(screen.getByTestId("display-cost-button")).toBeInTheDocument(); expect(screen.getByTestId("show-agent-tools-button")).toBeInTheDocument(); - expect(screen.getByTestId("show-microagents-button")).toBeInTheDocument(); + expect(screen.getByTestId("show-skills-button")).toBeInTheDocument(); expect( screen.getByTestId("export-conversation-button"), ).toBeInTheDocument(); @@ -321,9 +321,7 @@ describe("ConversationNameContextMenu", () => { expect( screen.queryByTestId("show-agent-tools-button"), ).not.toBeInTheDocument(); - expect( - screen.queryByTestId("show-microagents-button"), - ).not.toBeInTheDocument(); + expect(screen.queryByTestId("show-skills-button")).not.toBeInTheDocument(); expect( screen.queryByTestId("export-conversation-button"), ).not.toBeInTheDocument(); @@ -410,19 +408,19 @@ describe("ConversationNameContextMenu", () => { it("should call show microagents handler when show microagents button is clicked", async () => { const user = userEvent.setup(); - const onShowMicroagents = vi.fn(); + const onShowSkills = vi.fn(); renderWithProviders( , ); - const showMicroagentsButton = screen.getByTestId("show-microagents-button"); + const showMicroagentsButton = screen.getByTestId("show-skills-button"); await user.click(showMicroagentsButton); - expect(onShowMicroagents).toHaveBeenCalledTimes(1); + expect(onShowSkills).toHaveBeenCalledTimes(1); }); it("should call export conversation handler when export conversation button is clicked", async () => { @@ -519,7 +517,7 @@ describe("ConversationNameContextMenu", () => { onStop: vi.fn(), onDisplayCost: vi.fn(), onShowAgentTools: vi.fn(), - onShowMicroagents: vi.fn(), + onShowSkills: vi.fn(), onExportConversation: vi.fn(), onDownloadViaVSCode: vi.fn(), }; @@ -541,8 +539,8 @@ describe("ConversationNameContextMenu", () => { expect(screen.getByTestId("show-agent-tools-button")).toHaveTextContent( "Show Agent Tools", ); - expect(screen.getByTestId("show-microagents-button")).toHaveTextContent( - "Show Microagents", + expect(screen.getByTestId("show-skills-button")).toHaveTextContent( + "Show Skills", ); expect(screen.getByTestId("export-conversation-button")).toHaveTextContent( "Export Conversation", diff --git a/frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx b/frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx deleted file mode 100644 index 858c07207d..0000000000 --- a/frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { renderWithProviders } from "test-utils"; -import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal"; -import ConversationService from "#/api/conversation-service/conversation-service.api"; -import { AgentState } from "#/types/agent-state"; -import { useAgentState } from "#/hooks/use-agent-state"; - -// Mock the agent state hook -vi.mock("#/hooks/use-agent-state", () => ({ - useAgentState: vi.fn(), -})); - -// Mock the conversation ID hook -vi.mock("#/hooks/use-conversation-id", () => ({ - useConversationId: () => ({ conversationId: "test-conversation-id" }), -})); - -describe("MicroagentsModal - Refresh Button", () => { - const mockOnClose = vi.fn(); - const conversationId = "test-conversation-id"; - - const defaultProps = { - onClose: mockOnClose, - conversationId, - }; - - const mockMicroagents = [ - { - name: "Test Agent 1", - type: "repo" as const, - triggers: ["test", "example"], - content: "This is test content for agent 1", - }, - { - name: "Test Agent 2", - type: "knowledge" as const, - triggers: ["help", "support"], - content: "This is test content for agent 2", - }, - ]; - - beforeEach(() => { - // Reset all mocks before each test - vi.clearAllMocks(); - - // Setup default mock for getMicroagents - vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({ - microagents: mockMicroagents, - }); - - // Mock the agent state to return a ready state - vi.mocked(useAgentState).mockReturnValue({ - curAgentState: AgentState.AWAITING_USER_INPUT, - }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("Refresh Button Rendering", () => { - it("should render the refresh button with correct text and test ID", async () => { - renderWithProviders(); - - // Wait for the component to load and render the refresh button - const refreshButton = await screen.findByTestId("refresh-microagents"); - expect(refreshButton).toBeInTheDocument(); - expect(refreshButton).toHaveTextContent("BUTTON$REFRESH"); - }); - }); - - describe("Refresh Button Functionality", () => { - it("should call refetch when refresh button is clicked", async () => { - const user = userEvent.setup(); - const refreshSpy = vi.spyOn(ConversationService, "getMicroagents"); - - renderWithProviders(); - - // Wait for the component to load and render the refresh button - const refreshButton = await screen.findByTestId("refresh-microagents"); - - refreshSpy.mockClear(); - - await user.click(refreshButton); - - expect(refreshSpy).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/frontend/__tests__/components/modals/skills/skill-modal.test.tsx b/frontend/__tests__/components/modals/skills/skill-modal.test.tsx new file mode 100644 index 0000000000..33ab5098c8 --- /dev/null +++ b/frontend/__tests__/components/modals/skills/skill-modal.test.tsx @@ -0,0 +1,394 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { renderWithProviders } from "test-utils"; +import { SkillsModal } from "#/components/features/conversation-panel/skills-modal"; +import ConversationService from "#/api/conversation-service/conversation-service.api"; +import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api"; +import { AgentState } from "#/types/agent-state"; +import { useAgentState } from "#/hooks/use-agent-state"; +import SettingsService from "#/api/settings-service/settings-service.api"; + +// Mock the agent state hook +vi.mock("#/hooks/use-agent-state", () => ({ + useAgentState: vi.fn(), +})); + +// Mock the conversation ID hook +vi.mock("#/hooks/use-conversation-id", () => ({ + useConversationId: () => ({ conversationId: "test-conversation-id" }), +})); + +describe("SkillsModal - Refresh Button", () => { + const mockOnClose = vi.fn(); + const conversationId = "test-conversation-id"; + + const defaultProps = { + onClose: mockOnClose, + conversationId, + }; + + const mockSkills = [ + { + name: "Test Agent 1", + type: "repo" as const, + triggers: ["test", "example"], + content: "This is test content for agent 1", + }, + { + name: "Test Agent 2", + type: "knowledge" as const, + triggers: ["help", "support"], + content: "This is test content for agent 2", + }, + ]; + + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + + // Setup default mock for getMicroagents (V0) + vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({ + microagents: mockSkills, + }); + + // Mock the agent state to return a ready state + vi.mocked(useAgentState).mockReturnValue({ + curAgentState: AgentState.AWAITING_USER_INPUT, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("Refresh Button Rendering", () => { + it("should render the refresh button with correct text and test ID", async () => { + renderWithProviders(); + + // Wait for the component to load and render the refresh button + const refreshButton = await screen.findByTestId("refresh-skills"); + expect(refreshButton).toBeInTheDocument(); + expect(refreshButton).toHaveTextContent("BUTTON$REFRESH"); + }); + }); + + describe("Refresh Button Functionality", () => { + it("should call refetch when refresh button is clicked", async () => { + const user = userEvent.setup(); + const refreshSpy = vi.spyOn(ConversationService, "getMicroagents"); + + renderWithProviders(); + + // Wait for the component to load and render the refresh button + const refreshButton = await screen.findByTestId("refresh-skills"); + + // Clear previous calls to only track the click + refreshSpy.mockClear(); + + await user.click(refreshButton); + + // Verify the refresh triggered a new API call + expect(refreshSpy).toHaveBeenCalled(); + }); + }); +}); + +describe("useConversationSkills - V1 API Integration", () => { + const conversationId = "test-conversation-id"; + + const mockMicroagents = [ + { + name: "V0 Test Agent", + type: "repo" as const, + triggers: ["v0"], + content: "V0 skill content", + }, + ]; + + const mockSkills = [ + { + name: "V1 Test Skill", + type: "knowledge" as const, + triggers: ["v1", "skill"], + content: "V1 skill content", + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + + // Mock agent state + vi.mocked(useAgentState).mockReturnValue({ + curAgentState: AgentState.AWAITING_USER_INPUT, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("V0 API Usage (v1_enabled: false)", () => { + it("should call v0 ConversationService.getMicroagents when v1_enabled is false", async () => { + // Arrange + const getMicroagentsSpy = vi + .spyOn(ConversationService, "getMicroagents") + .mockResolvedValue({ microagents: mockMicroagents }); + + vi.spyOn(SettingsService, "getSettings").mockResolvedValue({ + v1_enabled: false, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + // Act + renderWithProviders(); + + // Assert + await screen.findByText("V0 Test Agent"); + expect(getMicroagentsSpy).toHaveBeenCalledWith(conversationId); + expect(getMicroagentsSpy).toHaveBeenCalledTimes(1); + }); + + it("should display v0 skills correctly", async () => { + // Arrange + vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({ + microagents: mockMicroagents, + }); + + vi.spyOn(SettingsService, "getSettings").mockResolvedValue({ + v1_enabled: false, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + // Act + renderWithProviders(); + + // Assert + const agentName = await screen.findByText("V0 Test Agent"); + expect(agentName).toBeInTheDocument(); + }); + }); + + describe("V1 API Usage (v1_enabled: true)", () => { + it("should call v1 V1ConversationService.getSkills when v1_enabled is true", async () => { + // Arrange + const getSkillsSpy = vi + .spyOn(V1ConversationService, "getSkills") + .mockResolvedValue({ skills: mockSkills }); + + vi.spyOn(SettingsService, "getSettings").mockResolvedValue({ + v1_enabled: true, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + // Act + renderWithProviders(); + + // Assert + await screen.findByText("V1 Test Skill"); + expect(getSkillsSpy).toHaveBeenCalledWith(conversationId); + expect(getSkillsSpy).toHaveBeenCalledTimes(1); + }); + + it("should display v1 skills correctly", async () => { + // Arrange + vi.spyOn(V1ConversationService, "getSkills").mockResolvedValue({ + skills: mockSkills, + }); + + vi.spyOn(SettingsService, "getSettings").mockResolvedValue({ + v1_enabled: true, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + // Act + renderWithProviders(); + + // Assert + const skillName = await screen.findByText("V1 Test Skill"); + expect(skillName).toBeInTheDocument(); + }); + + it("should use v1 API when v1_enabled is true", async () => { + // Arrange + vi.spyOn(SettingsService, "getSettings").mockResolvedValue({ + v1_enabled: true, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + const getSkillsSpy = vi + .spyOn(V1ConversationService, "getSkills") + .mockResolvedValue({ + skills: mockSkills, + }); + + // Act + renderWithProviders(); + + // Assert + await screen.findByText("V1 Test Skill"); + // Verify v1 API was called + expect(getSkillsSpy).toHaveBeenCalledWith(conversationId); + }); + }); + + describe("API Switching on Settings Change", () => { + it("should refetch using different API when v1_enabled setting changes", async () => { + // Arrange + const getMicroagentsSpy = vi + .spyOn(ConversationService, "getMicroagents") + .mockResolvedValue({ microagents: mockMicroagents }); + const getSkillsSpy = vi + .spyOn(V1ConversationService, "getSkills") + .mockResolvedValue({ skills: mockSkills }); + + const settingsSpy = vi + .spyOn(SettingsService, "getSettings") + .mockResolvedValue({ + v1_enabled: false, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + // Act - Initial render with v1_enabled: false + const { rerender } = renderWithProviders( + , + ); + + // Assert - v0 API called initially + await screen.findByText("V0 Test Agent"); + expect(getMicroagentsSpy).toHaveBeenCalledWith(conversationId); + + // Arrange - Change settings to v1_enabled: true + settingsSpy.mockResolvedValue({ + v1_enabled: true, + llm_model: "test-model", + llm_base_url: "", + agent: "test-agent", + language: "en", + llm_api_key: null, + llm_api_key_set: false, + search_api_key_set: false, + confirmation_mode: false, + security_analyzer: null, + remote_runtime_resource_factor: null, + provider_tokens_set: {}, + enable_default_condenser: false, + condenser_max_size: null, + enable_sound_notifications: false, + enable_proactive_conversation_starters: false, + enable_solvability_analysis: false, + user_consents_to_analytics: null, + max_budget_per_task: null, + }); + + // Act - Force re-render + rerender(); + + // Assert - v1 API should be called after settings change + await screen.findByText("V1 Test Skill"); + expect(getSkillsSpy).toHaveBeenCalledWith(conversationId); + }); + }); +}); diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts index bd37fa8180..d2f8f51ff5 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts @@ -11,6 +11,7 @@ import type { V1AppConversationStartTask, V1AppConversationStartTaskPage, V1AppConversation, + GetSkillsResponse, } from "./v1-conversation-service.types"; class V1ConversationService { @@ -315,6 +316,18 @@ class V1ConversationService { ); return data; } + + /** + * Get all skills associated with a V1 conversation + * @param conversationId The conversation ID + * @returns The available skills associated with the conversation + */ + static async getSkills(conversationId: string): Promise { + const { data } = await openHands.get( + `/api/v1/app-conversations/${conversationId}/skills`, + ); + return data; + } } export default V1ConversationService; diff --git a/frontend/src/api/conversation-service/v1-conversation-service.types.ts b/frontend/src/api/conversation-service/v1-conversation-service.types.ts index 621283c274..7c8b04ccbf 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.types.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.types.ts @@ -99,3 +99,14 @@ export interface V1AppConversation { conversation_url: string | null; session_api_key: string | null; } + +export interface Skill { + name: string; + type: "repo" | "knowledge"; + content: string; + triggers: string[]; +} + +export interface GetSkillsResponse { + skills: Skill[]; +} diff --git a/frontend/src/components/features/controls/tools-context-menu.tsx b/frontend/src/components/features/controls/tools-context-menu.tsx index 39330e25e4..2089f95111 100644 --- a/frontend/src/components/features/controls/tools-context-menu.tsx +++ b/frontend/src/components/features/controls/tools-context-menu.tsx @@ -26,14 +26,14 @@ const contextMenuListItemClassName = cn( interface ToolsContextMenuProps { onClose: () => void; - onShowMicroagents: (event: React.MouseEvent) => void; + onShowSkills: (event: React.MouseEvent) => void; onShowAgentTools: (event: React.MouseEvent) => void; shouldShowAgentTools?: boolean; } export function ToolsContextMenu({ onClose, - onShowMicroagents, + onShowSkills, onShowAgentTools, shouldShowAgentTools = true, }: ToolsContextMenuProps) { @@ -41,7 +41,6 @@ export function ToolsContextMenu({ const { data: conversation } = useActiveConversation(); const { providers } = useUserProviders(); - // TODO: Hide microagent menu items for V1 conversations // This is a temporary measure and may be re-enabled in the future const isV1Conversation = conversation?.conversation_version === "V1"; @@ -130,20 +129,17 @@ export function ToolsContextMenu({ {(!isV1Conversation || shouldShowAgentTools) && } - {/* Show Available Microagents - Hidden for V1 conversations */} - {!isV1Conversation && ( - - } - text={t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)} - className={CONTEXT_MENU_ICON_TEXT_CLASSNAME} - /> - - )} + + } + text={t(I18nKey.CONVERSATION$SHOW_SKILLS)} + className={CONTEXT_MENU_ICON_TEXT_CLASSNAME} + /> + {/* Show Agent Tools and Metadata - Only show if system message is available */} {shouldShowAgentTools && ( diff --git a/frontend/src/components/features/controls/tools.tsx b/frontend/src/components/features/controls/tools.tsx index 56ef58bc8e..80994cbe65 100644 --- a/frontend/src/components/features/controls/tools.tsx +++ b/frontend/src/components/features/controls/tools.tsx @@ -7,7 +7,7 @@ import { ToolsContextMenu } from "./tools-context-menu"; import { useConversationNameContextMenu } from "#/hooks/use-conversation-name-context-menu"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { SystemMessageModal } from "../conversation-panel/system-message-modal"; -import { MicroagentsModal } from "../conversation-panel/microagents-modal"; +import { SkillsModal } from "../conversation-panel/skills-modal"; export function Tools() { const { t } = useTranslation(); @@ -17,11 +17,11 @@ export function Tools() { const { handleShowAgentTools, - handleShowMicroagents, + handleShowSkills, systemModalVisible, setSystemModalVisible, - microagentsModalVisible, - setMicroagentsModalVisible, + skillsModalVisible, + setSkillsModalVisible, systemMessage, shouldShowAgentTools, } = useConversationNameContextMenu({ @@ -51,7 +51,7 @@ export function Tools() { {contextMenuOpen && ( setContextMenuOpen(false)} - onShowMicroagents={handleShowMicroagents} + onShowSkills={handleShowSkills} onShowAgentTools={handleShowAgentTools} shouldShowAgentTools={shouldShowAgentTools} /> @@ -64,9 +64,9 @@ export function Tools() { systemMessage={systemMessage ? systemMessage.args : null} /> - {/* Microagents Modal */} - {microagentsModalVisible && ( - setMicroagentsModalVisible(false)} /> + {/* Skills Modal */} + {skillsModalVisible && ( + setSkillsModalVisible(false)} /> )} ); diff --git a/frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx b/frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx deleted file mode 100644 index 63ea33152b..0000000000 --- a/frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - Trash, - Power, - Pencil, - Download, - Wallet, - Wrench, - Bot, -} from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; -import { cn } from "#/utils/utils"; -import { ContextMenu } from "#/ui/context-menu"; -import { ContextMenuListItem } from "../context-menu/context-menu-list-item"; -import { Divider } from "#/ui/divider"; -import { I18nKey } from "#/i18n/declaration"; -import { ContextMenuIconText } from "../context-menu/context-menu-icon-text"; -import { useActiveConversation } from "#/hooks/query/use-active-conversation"; - -interface ConversationCardContextMenuProps { - onClose: () => void; - onDelete?: (event: React.MouseEvent) => void; - onStop?: (event: React.MouseEvent) => void; - onEdit?: (event: React.MouseEvent) => void; - onDisplayCost?: (event: React.MouseEvent) => void; - onShowAgentTools?: (event: React.MouseEvent) => void; - onShowMicroagents?: (event: React.MouseEvent) => void; - onDownloadViaVSCode?: (event: React.MouseEvent) => void; - position?: "top" | "bottom"; -} - -export function ConversationCardContextMenu({ - onClose, - onDelete, - onStop, - onEdit, - onDisplayCost, - onShowAgentTools, - onShowMicroagents, - onDownloadViaVSCode, - position = "bottom", -}: ConversationCardContextMenuProps) { - const { t } = useTranslation(); - const ref = useClickOutsideElement(onClose); - const { data: conversation } = useActiveConversation(); - - // TODO: Hide microagent menu items for V1 conversations - // This is a temporary measure and may be re-enabled in the future - const isV1Conversation = conversation?.conversation_version === "V1"; - - const hasEdit = Boolean(onEdit); - const hasDownload = Boolean(onDownloadViaVSCode); - const hasTools = Boolean(onShowAgentTools || onShowMicroagents); - const hasInfo = Boolean(onDisplayCost); - const hasControl = Boolean(onStop || onDelete); - - return ( - - {onEdit && ( - - - - )} - - {hasEdit && (hasDownload || hasTools || hasInfo || hasControl) && ( - - )} - - {onDownloadViaVSCode && ( - - - - )} - - {hasDownload && (hasTools || hasInfo || hasControl) && } - - {onShowAgentTools && ( - - - - )} - - {onShowMicroagents && !isV1Conversation && ( - - - - )} - - {hasTools && (hasInfo || hasControl) && } - - {onDisplayCost && ( - - - - )} - - {hasInfo && hasControl && } - - {onStop && ( - - - - )} - - {onDelete && ( - - - - )} - - ); -} diff --git a/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx b/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx index 6565a83a10..30a7ec42cb 100644 --- a/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx @@ -22,7 +22,7 @@ interface ConversationCardContextMenuProps { onEdit?: (event: React.MouseEvent) => void; onDisplayCost?: (event: React.MouseEvent) => void; onShowAgentTools?: (event: React.MouseEvent) => void; - onShowMicroagents?: (event: React.MouseEvent) => void; + onShowSkills?: (event: React.MouseEvent) => void; onDownloadViaVSCode?: (event: React.MouseEvent) => void; position?: "top" | "bottom"; } @@ -37,7 +37,7 @@ export function ConversationCardContextMenu({ onEdit, onDisplayCost, onShowAgentTools, - onShowMicroagents, + onShowSkills, onDownloadViaVSCode, position = "bottom", }: ConversationCardContextMenuProps) { @@ -96,15 +96,15 @@ export function ConversationCardContextMenu({ /> ), - onShowMicroagents && ( + onShowSkills && ( } - text={t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)} + text={t(I18nKey.CONVERSATION$SHOW_SKILLS)} /> ), diff --git a/frontend/src/components/features/conversation-panel/microagent-content.tsx b/frontend/src/components/features/conversation-panel/skill-content.tsx similarity index 76% rename from frontend/src/components/features/conversation-panel/microagent-content.tsx rename to frontend/src/components/features/conversation-panel/skill-content.tsx index fad0485607..9303047e3a 100644 --- a/frontend/src/components/features/conversation-panel/microagent-content.tsx +++ b/frontend/src/components/features/conversation-panel/skill-content.tsx @@ -3,17 +3,17 @@ import { I18nKey } from "#/i18n/declaration"; import { Typography } from "#/ui/typography"; import { Pre } from "#/ui/pre"; -interface MicroagentContentProps { +interface SkillContentProps { content: string; } -export function MicroagentContent({ content }: MicroagentContentProps) { +export function SkillContent({ content }: SkillContentProps) { const { t } = useTranslation(); return (
- {t(I18nKey.MICROAGENTS_MODAL$CONTENT)} + {t(I18nKey.COMMON$CONTENT)}
-        {content || t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
+        {content || t(I18nKey.SKILLS_MODAL$NO_CONTENT)}
       
); diff --git a/frontend/src/components/features/conversation-panel/microagent-item.tsx b/frontend/src/components/features/conversation-panel/skill-item.tsx similarity index 65% rename from frontend/src/components/features/conversation-panel/microagent-item.tsx rename to frontend/src/components/features/conversation-panel/skill-item.tsx index d23febb099..c76bf10be9 100644 --- a/frontend/src/components/features/conversation-panel/microagent-item.tsx +++ b/frontend/src/components/features/conversation-panel/skill-item.tsx @@ -1,35 +1,31 @@ import { ChevronDown, ChevronRight } from "lucide-react"; -import { Microagent } from "#/api/open-hands.types"; import { Typography } from "#/ui/typography"; -import { MicroagentTriggers } from "./microagent-triggers"; -import { MicroagentContent } from "./microagent-content"; +import { SkillTriggers } from "./skill-triggers"; +import { SkillContent } from "./skill-content"; +import { Skill } from "#/api/conversation-service/v1-conversation-service.types"; -interface MicroagentItemProps { - agent: Microagent; +interface SkillItemProps { + skill: Skill; isExpanded: boolean; onToggle: (agentName: string) => void; } -export function MicroagentItem({ - agent, - isExpanded, - onToggle, -}: MicroagentItemProps) { +export function SkillItem({ skill, isExpanded, onToggle }: SkillItemProps) { return (