-
- {message}
-
+
+ {actions?.map((action, index) => (
+
+ ))}
+
+
+
+
+
{children}
diff --git a/frontend/src/components/features/chat/event-message.tsx b/frontend/src/components/features/chat/event-message.tsx
index 537042e69a..98d63fcc72 100644
--- a/frontend/src/components/features/chat/event-message.tsx
+++ b/frontend/src/components/features/chat/event-message.tsx
@@ -19,6 +19,8 @@ import { MCPObservationContent } from "./mcp-observation-content";
import { getObservationResult } from "./event-content-helpers/get-observation-result";
import { getEventContent } from "./event-content-helpers/get-event-content";
import { GenericEventMessage } from "./generic-event-message";
+import { MicroagentStatus } from "#/types/microagent-status";
+import { MicroagentStatusIndicator } from "./microagent/microagent-status-indicator";
import { FileList } from "../files/file-list";
import { parseMessageFromEvent } from "./event-content-helpers/parse-message-from-event";
import { LikertScale } from "../feedback/likert-scale";
@@ -35,6 +37,13 @@ interface EventMessageProps {
hasObservationPair: boolean;
isAwaitingUserConfirmation: boolean;
isLastMessage: boolean;
+ microagentStatus?: MicroagentStatus | null;
+ microagentConversationId?: string;
+ microagentPRUrl?: string;
+ actions?: Array<{
+ icon: React.ReactNode;
+ onClick: () => void;
+ }>;
isInLast10Actions: boolean;
}
@@ -43,6 +52,10 @@ export function EventMessage({
hasObservationPair,
isAwaitingUserConfirmation,
isLastMessage,
+ microagentStatus,
+ microagentConversationId,
+ microagentPRUrl,
+ actions,
isInLast10Actions,
}: EventMessageProps) {
const shouldShowConfirmationButtons =
@@ -82,27 +95,66 @@ export function EventMessage({
if (isErrorObservation(event)) {
return (
- <>
+
+ {microagentStatus && actions && (
+
+ )}
{renderLikertScale()}
- >
+
);
}
if (hasObservationPair && isOpenHandsAction(event)) {
if (hasThoughtProperty(event.args)) {
- return
;
+ return (
+
+
+ {microagentStatus && actions && (
+
+ )}
+
+ );
}
- return null;
+ return microagentStatus && actions ? (
+
+ ) : null;
}
if (isFinishAction(event)) {
return (
<>
-
+
+ {microagentStatus && actions && (
+
+ )}
{renderLikertScale()}
>
);
@@ -112,8 +164,8 @@ export function EventMessage({
const message = parseMessageFromEvent(event);
return (
- <>
-
+
+
{event.args.image_urls && event.args.image_urls.length > 0 && (
)}
@@ -122,15 +174,26 @@ export function EventMessage({
)}
{shouldShowConfirmationButtons && }
+ {microagentStatus && actions && (
+
+ )}
{isAssistantMessage(event) &&
event.action === "message" &&
renderLikertScale()}
- >
+
);
}
if (isRejectObservation(event)) {
- return ;
+ return (
+
+
+
+ );
}
if (isMcpObservation(event)) {
diff --git a/frontend/src/components/features/chat/messages.tsx b/frontend/src/components/features/chat/messages.tsx
index 04b3ad9824..ecfc7546d6 100644
--- a/frontend/src/components/features/chat/messages.tsx
+++ b/frontend/src/components/features/chat/messages.tsx
@@ -1,10 +1,28 @@
import React from "react";
+import { createPortal } from "react-dom";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
-import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
+import {
+ isOpenHandsAction,
+ isOpenHandsObservation,
+ isOpenHandsEvent,
+ isAgentStateChangeObservation,
+ isFinishAction,
+} from "#/types/core/guards";
import { EventMessage } from "./event-message";
import { ChatMessage } from "./chat-message";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
+import { LaunchMicroagentModal } from "./microagent/launch-microagent-modal";
+import { useUserConversation } from "#/hooks/query/use-user-conversation";
+import { useConversationId } from "#/hooks/use-conversation-id";
+import { useCreateConversationAndSubscribeMultiple } from "#/hooks/use-create-conversation-and-subscribe-multiple";
+import {
+ MicroagentStatus,
+ EventMicroagentStatus,
+} from "#/types/microagent-status";
+import { AgentState } from "#/types/agent-state";
+import { getFirstPRUrl } from "#/utils/parse-pr-url";
+import MemoryIcon from "#/icons/memory_icon.svg?react";
interface MessagesProps {
messages: (OpenHandsAction | OpenHandsObservation)[];
@@ -13,10 +31,23 @@ interface MessagesProps {
export const Messages: React.FC = React.memo(
({ messages, isAwaitingUserConfirmation }) => {
+ const { createConversationAndSubscribe, isPending } =
+ useCreateConversationAndSubscribeMultiple();
const { getOptimisticUserMessage } = useOptimisticUserMessage();
+ const { conversationId } = useConversationId();
+ const { data: conversation } = useUserConversation(conversationId);
const optimisticUserMessage = getOptimisticUserMessage();
+ const [selectedEventId, setSelectedEventId] = React.useState(
+ null,
+ );
+ const [showLaunchMicroagentModal, setShowLaunchMicroagentModal] =
+ React.useState(false);
+ const [microagentStatuses, setMicroagentStatuses] = React.useState<
+ EventMicroagentStatus[]
+ >([]);
+
const actionHasObservationPair = React.useCallback(
(event: OpenHandsAction | OpenHandsObservation): boolean => {
if (isOpenHandsAction(event)) {
@@ -30,6 +61,139 @@ export const Messages: React.FC = React.memo(
[messages],
);
+ const getMicroagentStatusForEvent = React.useCallback(
+ (eventId: number): MicroagentStatus | null => {
+ const statusEntry = microagentStatuses.find(
+ (entry) => entry.eventId === eventId,
+ );
+ return statusEntry?.status || null;
+ },
+ [microagentStatuses],
+ );
+
+ const getMicroagentConversationIdForEvent = React.useCallback(
+ (eventId: number): string | undefined => {
+ const statusEntry = microagentStatuses.find(
+ (entry) => entry.eventId === eventId,
+ );
+ return statusEntry?.conversationId || undefined;
+ },
+ [microagentStatuses],
+ );
+
+ const getMicroagentPRUrlForEvent = React.useCallback(
+ (eventId: number): string | undefined => {
+ const statusEntry = microagentStatuses.find(
+ (entry) => entry.eventId === eventId,
+ );
+ return statusEntry?.prUrl || undefined;
+ },
+ [microagentStatuses],
+ );
+
+ 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) =>
+ statusEntry.conversationId === microagentConversationId
+ ? { ...statusEntry, status: MicroagentStatus.ERROR }
+ : statusEntry,
+ ),
+ );
+ } else if (
+ isOpenHandsEvent(socketEvent) &&
+ isAgentStateChangeObservation(socketEvent)
+ ) {
+ if (socketEvent.extras.agent_state === AgentState.FINISHED) {
+ setMicroagentStatuses((prev) =>
+ prev.map((statusEntry) =>
+ statusEntry.conversationId === microagentConversationId
+ ? { ...statusEntry, status: MicroagentStatus.COMPLETED }
+ : statusEntry,
+ ),
+ );
+ }
+ } else if (
+ isOpenHandsEvent(socketEvent) &&
+ isFinishAction(socketEvent)
+ ) {
+ // Check if the finish action contains a PR URL
+ const prUrl = getFirstPRUrl(socketEvent.args.final_thought || "");
+ if (prUrl) {
+ setMicroagentStatuses((prev) =>
+ prev.map((statusEntry) =>
+ statusEntry.conversationId === microagentConversationId
+ ? {
+ ...statusEntry,
+ status: MicroagentStatus.COMPLETED,
+ prUrl,
+ }
+ : statusEntry,
+ ),
+ );
+ }
+ }
+ },
+ [setMicroagentStatuses],
+ );
+
+ const handleLaunchMicroagent = (
+ query: string,
+ target: string,
+ triggers: string[],
+ ) => {
+ const conversationInstructions = `Target file: ${target}\n\nDescription: ${query}\n\nTriggers: ${triggers.join(", ")}`;
+ if (
+ !conversation ||
+ !conversation.selected_repository ||
+ !conversation.selected_branch ||
+ !conversation.git_provider ||
+ !selectedEventId
+ ) {
+ return;
+ }
+
+ createConversationAndSubscribe({
+ query,
+ conversationInstructions,
+ repository: {
+ name: conversation.selected_repository,
+ branch: conversation.selected_branch,
+ gitProvider: conversation.git_provider,
+ },
+ onSuccessCallback: (newConversationId: string) => {
+ setShowLaunchMicroagentModal(false);
+ // Update status with conversation ID
+ setMicroagentStatuses((prev) => [
+ ...prev.filter((status) => status.eventId !== selectedEventId),
+ {
+ eventId: selectedEventId,
+ conversationId: newConversationId,
+ status: MicroagentStatus.CREATING,
+ },
+ ]);
+ },
+ onEventCallback: (socketEvent: unknown, newConversationId: string) => {
+ handleMicroagentEvent(socketEvent, newConversationId);
+ },
+ });
+ };
+
return (
<>
{messages.map((message, index) => (
@@ -39,6 +203,26 @@ export const Messages: React.FC = React.memo(
hasObservationPair={actionHasObservationPair(message)}
isAwaitingUserConfirmation={isAwaitingUserConfirmation}
isLastMessage={messages.length - 1 === index}
+ microagentStatus={getMicroagentStatusForEvent(message.id)}
+ microagentConversationId={getMicroagentConversationIdForEvent(
+ message.id,
+ )}
+ microagentPRUrl={getMicroagentPRUrlForEvent(message.id)}
+ actions={
+ conversation?.selected_repository
+ ? [
+ {
+ icon: (
+
+ ),
+ onClick: () => {
+ setSelectedEventId(message.id);
+ setShowLaunchMicroagentModal(true);
+ },
+ },
+ ]
+ : undefined
+ }
isInLast10Actions={messages.length - 1 - index < 10}
/>
))}
@@ -46,6 +230,21 @@ export const Messages: React.FC = React.memo(
{optimisticUserMessage && (
)}
+ {conversation?.selected_repository &&
+ showLaunchMicroagentModal &&
+ selectedEventId &&
+ createPortal(
+ setShowLaunchMicroagentModal(false)}
+ onLaunch={handleLaunchMicroagent}
+ selectedRepo={
+ conversation.selected_repository.split("/").pop() || ""
+ }
+ eventId={selectedEventId}
+ isLoading={isPending}
+ />,
+ document.getElementById("modal-portal-exit") || document.body,
+ )}
>
);
},
diff --git a/frontend/src/components/features/chat/microagent/launch-microagent-modal.tsx b/frontend/src/components/features/chat/microagent/launch-microagent-modal.tsx
new file mode 100644
index 0000000000..00888d99d2
--- /dev/null
+++ b/frontend/src/components/features/chat/microagent/launch-microagent-modal.tsx
@@ -0,0 +1,163 @@
+import React from "react";
+import { FaCircleInfo } from "react-icons/fa6";
+import { useTranslation } from "react-i18next";
+import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
+import { ModalBody } from "#/components/shared/modals/modal-body";
+import { BrandButton } from "../../settings/brand-button";
+import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
+import { BadgeInput } from "#/components/shared/inputs/badge-input";
+import { cn } from "#/utils/utils";
+import CloseIcon from "#/icons/close.svg?react";
+import { useMicroagentPrompt } from "#/hooks/query/use-microagent-prompt";
+import { useHandleRuntimeActive } from "#/hooks/use-handle-runtime-active";
+import { LoadingMicroagentBody } from "./loading-microagent-body";
+import { LoadingMicroagentTextarea } from "./loading-microagent-textarea";
+import { useGetMicroagents } from "#/hooks/query/use-get-microagents";
+
+interface LaunchMicroagentModalProps {
+ onClose: () => void;
+ onLaunch: (query: string, target: string, triggers: string[]) => void;
+ eventId: number;
+ isLoading: boolean;
+ selectedRepo: string;
+}
+
+export function LaunchMicroagentModal({
+ onClose,
+ onLaunch,
+ eventId,
+ isLoading,
+ selectedRepo,
+}: LaunchMicroagentModalProps) {
+ const { t } = useTranslation();
+ const { runtimeActive } = useHandleRuntimeActive();
+ const { data: prompt, isLoading: promptIsLoading } =
+ useMicroagentPrompt(eventId);
+
+ const { data: microagents, isLoading: microagentsIsLoading } =
+ useGetMicroagents(`${selectedRepo}/.openhands/microagents`);
+
+ const [triggers, setTriggers] = React.useState([]);
+
+ const formAction = (formData: FormData) => {
+ const query = formData.get("query-input")?.toString();
+ const target = formData.get("target-input")?.toString();
+
+ if (query && target) {
+ onLaunch(query, target, triggers);
+ }
+ };
+
+ const onSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+ formAction(formData);
+ };
+
+ return (
+
+ {!runtimeActive && }
+ {runtimeActive && (
+
+
+
+ {t("MICROAGENT$ADD_TO_MICROAGENT")}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/features/chat/microagent/loading-microagent-body.tsx b/frontend/src/components/features/chat/microagent/loading-microagent-body.tsx
new file mode 100644
index 0000000000..5f960a9e93
--- /dev/null
+++ b/frontend/src/components/features/chat/microagent/loading-microagent-body.tsx
@@ -0,0 +1,16 @@
+import { Spinner } from "@heroui/react";
+import { useTranslation } from "react-i18next";
+import { ModalBody } from "#/components/shared/modals/modal-body";
+
+export function LoadingMicroagentBody() {
+ const { t } = useTranslation();
+ return (
+
+
+ {t("MICROAGENT$ADD_TO_MICROAGENT")}
+
+
+ {t("MICROAGENT$WAIT_FOR_RUNTIME")}
+
+ );
+}
diff --git a/frontend/src/components/features/chat/microagent/loading-microagent-textarea.tsx b/frontend/src/components/features/chat/microagent/loading-microagent-textarea.tsx
new file mode 100644
index 0000000000..03b4f11771
--- /dev/null
+++ b/frontend/src/components/features/chat/microagent/loading-microagent-textarea.tsx
@@ -0,0 +1,20 @@
+import { useTranslation } from "react-i18next";
+import { cn } from "#/utils/utils";
+
+export function LoadingMicroagentTextarea() {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/features/chat/microagent/microagent-status-indicator.tsx b/frontend/src/components/features/chat/microagent/microagent-status-indicator.tsx
new file mode 100644
index 0000000000..319e1be01a
--- /dev/null
+++ b/frontend/src/components/features/chat/microagent/microagent-status-indicator.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Spinner } from "@heroui/react";
+import { MicroagentStatus } from "#/types/microagent-status";
+import { SuccessIndicator } from "../success-indicator";
+
+interface MicroagentStatusIndicatorProps {
+ status: MicroagentStatus;
+ conversationId?: string;
+ prUrl?: string;
+}
+
+export function MicroagentStatusIndicator({
+ status,
+ conversationId,
+ prUrl,
+}: MicroagentStatusIndicatorProps) {
+ const { t } = useTranslation();
+
+ const getStatusText = () => {
+ switch (status) {
+ case MicroagentStatus.CREATING:
+ return t("MICROAGENT$STATUS_CREATING");
+ case MicroagentStatus.COMPLETED:
+ // If there's a PR URL, show "View your PR" instead of the default completed message
+ return prUrl
+ ? t("MICROAGENT$VIEW_YOUR_PR")
+ : t("MICROAGENT$STATUS_COMPLETED");
+ case MicroagentStatus.ERROR:
+ return t("MICROAGENT$STATUS_ERROR");
+ default:
+ return "";
+ }
+ };
+
+ const getStatusIcon = () => {
+ switch (status) {
+ case MicroagentStatus.CREATING:
+ return ;
+ case MicroagentStatus.COMPLETED:
+ return ;
+ case MicroagentStatus.ERROR:
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const statusText = getStatusText();
+ const shouldShowAsLink = !!conversationId;
+ const shouldShowPRLink = !!prUrl;
+
+ const renderStatusText = () => {
+ if (shouldShowPRLink) {
+ return (
+
+ {statusText}
+
+ );
+ }
+
+ if (shouldShowAsLink) {
+ return (
+
+ {statusText}
+
+ );
+ }
+
+ return {statusText};
+ };
+
+ return (
+
+ {getStatusIcon()}
+ {renderStatusText()}
+
+ );
+}
diff --git a/frontend/src/components/features/chat/microagent/microagent-status-toast.tsx b/frontend/src/components/features/chat/microagent/microagent-status-toast.tsx
new file mode 100644
index 0000000000..e0e6d5aaae
--- /dev/null
+++ b/frontend/src/components/features/chat/microagent/microagent-status-toast.tsx
@@ -0,0 +1,138 @@
+import toast from "react-hot-toast";
+import { Spinner } from "@heroui/react";
+import { useTranslation } from "react-i18next";
+import { TOAST_OPTIONS } from "#/utils/custom-toast-handlers";
+import CloseIcon from "#/icons/close.svg?react";
+import { SuccessIndicator } from "../success-indicator";
+
+interface ConversationCreatedToastProps {
+ conversationId: string;
+ onClose: () => void;
+}
+
+function ConversationCreatedToast({
+ conversationId,
+ onClose,
+}: ConversationCreatedToastProps) {
+ const { t } = useTranslation();
+ return (
+
+ );
+}
+
+interface ConversationFinishedToastProps {
+ conversationId: string;
+ onClose: () => void;
+}
+
+function ConversationFinishedToast({
+ conversationId,
+ onClose,
+}: ConversationFinishedToastProps) {
+ const { t } = useTranslation();
+ return (
+
+ );
+}
+
+interface ConversationErroredToastProps {
+ errorMessage: string;
+ onClose: () => void;
+}
+
+function ConversationErroredToast({
+ errorMessage,
+ onClose,
+}: ConversationErroredToastProps) {
+ return (
+
+
+
{errorMessage}
+
+
+ );
+}
+
+export const renderConversationCreatedToast = (conversationId: string) =>
+ toast(
+ (t) => (
+ toast.dismiss(t.id)}
+ />
+ ),
+ {
+ ...TOAST_OPTIONS,
+ id: `status-${conversationId}`,
+ duration: 5000,
+ },
+ );
+
+export const renderConversationFinishedToast = (conversationId: string) =>
+ toast(
+ (t) => (
+ toast.dismiss(t.id)}
+ />
+ ),
+ {
+ ...TOAST_OPTIONS,
+ id: `status-${conversationId}`,
+ duration: 5000,
+ },
+ );
+
+export const renderConversationErroredToast = (
+ conversationId: string,
+ errorMessage: string,
+) =>
+ toast(
+ (t) => (
+ toast.dismiss(t.id)}
+ />
+ ),
+ {
+ ...TOAST_OPTIONS,
+ id: `status-${conversationId}`,
+ duration: 5000,
+ },
+ );
diff --git a/frontend/src/components/features/conversation-panel/conversation-card.tsx b/frontend/src/components/features/conversation-panel/conversation-card.tsx
index cdd0c4b680..de70b49cd9 100644
--- a/frontend/src/components/features/conversation-panel/conversation-card.tsx
+++ b/frontend/src/components/features/conversation-panel/conversation-card.tsx
@@ -409,10 +409,7 @@ export function ConversationCard({
/>
{microagentsModalVisible && (
- setMicroagentsModalVisible(false)}
- conversationId={conversationId}
- />
+ setMicroagentsModalVisible(false)} />
)}
>
);
diff --git a/frontend/src/components/features/conversation-panel/microagents-modal.tsx b/frontend/src/components/features/conversation-panel/microagents-modal.tsx
index ebe9a5a287..9457b43132 100644
--- a/frontend/src/components/features/conversation-panel/microagents-modal.tsx
+++ b/frontend/src/components/features/conversation-panel/microagents-modal.tsx
@@ -13,13 +13,9 @@ import { BrandButton } from "../settings/brand-button";
interface MicroagentsModalProps {
onClose: () => void;
- conversationId: string | undefined;
}
-export function MicroagentsModal({
- onClose,
- conversationId,
-}: MicroagentsModalProps) {
+export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
const { t } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [expandedAgents, setExpandedAgents] = useState>(
@@ -31,11 +27,7 @@ export function MicroagentsModal({
isError,
refetch,
isRefetching,
- } = useConversationMicroagents({
- agentState: curAgentState,
- conversationId,
- enabled: true,
- });
+ } = useConversationMicroagents();
const toggleAgent = (agentName: string) => {
setExpandedAgents((prev) => ({
diff --git a/frontend/src/components/features/home/home-header.tsx b/frontend/src/components/features/home/home-header.tsx
index cf04a167ae..f9dc71b513 100644
--- a/frontend/src/components/features/home/home-header.tsx
+++ b/frontend/src/components/features/home/home-header.tsx
@@ -1,10 +1,12 @@
import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
import { BrandButton } from "../settings/brand-button";
import AllHandsLogo from "#/assets/branding/all-hands-logo-spark.svg?react";
export function HomeHeader() {
+ const navigate = useNavigate();
const {
mutate: createConversation,
isPending,
@@ -28,7 +30,15 @@ export function HomeHeader() {
testId="header-launch-button"
variant="primary"
type="button"
- onClick={() => createConversation({})}
+ onClick={() =>
+ createConversation(
+ {},
+ {
+ onSuccess: (data) =>
+ navigate(`/conversations/${data.conversation_id}`),
+ },
+ )
+ }
isDisabled={isCreatingConversation}
>
{!isCreatingConversation && t("HOME$LAUNCH_FROM_SCRATCH")}
diff --git a/frontend/src/components/features/home/repo-selection-form.test.tsx b/frontend/src/components/features/home/repo-selection-form.test.tsx
deleted file mode 100644
index f97d592035..0000000000
--- a/frontend/src/components/features/home/repo-selection-form.test.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { describe, test, expect, vi, beforeEach } from "vitest";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { RepositorySelectionForm } from "./repo-selection-form";
-
-// Create mock functions
-const mockUseUserRepositories = vi.fn();
-const mockUseRepositoryBranches = vi.fn();
-const mockUseCreateConversation = vi.fn();
-const mockUseIsCreatingConversation = vi.fn();
-const mockUseTranslation = vi.fn();
-const mockUseAuth = vi.fn();
-
-// Setup default mock returns
-mockUseUserRepositories.mockReturnValue({
- data: [],
- isLoading: false,
- isError: false,
-});
-
-mockUseRepositoryBranches.mockReturnValue({
- data: [],
- isLoading: false,
- isError: false,
-});
-
-mockUseCreateConversation.mockReturnValue({
- mutate: vi.fn(),
- isPending: false,
- isSuccess: false,
-});
-
-mockUseIsCreatingConversation.mockReturnValue(false);
-
-mockUseTranslation.mockReturnValue({ t: (key: string) => key });
-
-mockUseAuth.mockReturnValue({
- isAuthenticated: true,
- isLoading: false,
- providersAreSet: true,
- user: {
- id: 1,
- login: "testuser",
- avatar_url: "https://example.com/avatar.png",
- name: "Test User",
- email: "test@example.com",
- company: "Test Company",
- },
- login: vi.fn(),
- logout: vi.fn(),
-});
-
-// Mock the modules
-vi.mock("#/hooks/query/use-user-repositories", () => ({
- useUserRepositories: () => mockUseUserRepositories(),
-}));
-
-vi.mock("#/hooks/query/use-repository-branches", () => ({
- useRepositoryBranches: () => mockUseRepositoryBranches(),
-}));
-
-vi.mock("#/hooks/mutation/use-create-conversation", () => ({
- useCreateConversation: () => mockUseCreateConversation(),
-}));
-
-vi.mock("#/hooks/use-is-creating-conversation", () => ({
- useIsCreatingConversation: () => mockUseIsCreatingConversation(),
-}));
-
-vi.mock("react-i18next", () => ({
- useTranslation: () => mockUseTranslation(),
-}));
-
-vi.mock("#/context/auth-context", () => ({
- useAuth: () => mockUseAuth(),
-}));
-
-const renderRepositorySelectionForm = () =>
- render(, {
- wrapper: ({ children }) => (
-
- {children}
-
- ),
- });
-
-describe("RepositorySelectionForm", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- test("shows loading indicator when repositories are being fetched", () => {
- // Setup loading state
- mockUseUserRepositories.mockReturnValue({
- data: undefined,
- isLoading: true,
- isError: false,
- });
-
- renderRepositorySelectionForm();
-
- // Check if loading indicator is displayed
- expect(screen.getByTestId("repo-dropdown-loading")).toBeInTheDocument();
- expect(screen.getByText("HOME$LOADING_REPOSITORIES")).toBeInTheDocument();
- });
-
- test("shows dropdown when repositories are loaded", () => {
- // Setup loaded repositories
- mockUseUserRepositories.mockReturnValue({
- data: [
- {
- id: 1,
- full_name: "user/repo1",
- git_provider: "github",
- is_public: true,
- },
- {
- id: 2,
- full_name: "user/repo2",
- git_provider: "github",
- is_public: true,
- },
- ],
- isLoading: false,
- isError: false,
- });
-
- renderRepositorySelectionForm();
-
- // Check if dropdown is displayed
- expect(screen.getByTestId("repo-dropdown")).toBeInTheDocument();
- });
-
- test("shows error message when repository fetch fails", () => {
- // Setup error state
- mockUseUserRepositories.mockReturnValue({
- data: undefined,
- isLoading: false,
- isError: true,
- error: new Error("Failed to fetch repositories"),
- });
-
- renderRepositorySelectionForm();
-
- // Check if error message is displayed
- expect(screen.getByTestId("repo-dropdown-error")).toBeInTheDocument();
- expect(
- screen.getByText("HOME$FAILED_TO_LOAD_REPOSITORIES"),
- ).toBeInTheDocument();
- });
-});
diff --git a/frontend/src/components/features/home/repo-selection-form.tsx b/frontend/src/components/features/home/repo-selection-form.tsx
index 45b219fdf9..a5e671f5c7 100644
--- a/frontend/src/components/features/home/repo-selection-form.tsx
+++ b/frontend/src/components/features/home/repo-selection-form.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { useRepositoryBranches } from "#/hooks/query/use-repository-branches";
@@ -25,6 +26,7 @@ interface RepositorySelectionFormProps {
export function RepositorySelectionForm({
onRepoSelection,
}: RepositorySelectionFormProps) {
+ const navigate = useNavigate();
const [selectedRepository, setSelectedRepository] =
React.useState(null);
const [selectedBranch, setSelectedBranch] = React.useState(
@@ -208,10 +210,19 @@ export function RepositorySelectionForm({
isRepositoriesError
}
onClick={() =>
- createConversation({
- selectedRepository,
- selected_branch: selectedBranch?.name,
- })
+ createConversation(
+ {
+ repository: {
+ name: selectedRepository?.full_name || "",
+ gitProvider: selectedRepository?.git_provider || "github",
+ branch: selectedBranch?.name || "main",
+ },
+ },
+ {
+ onSuccess: (data) =>
+ navigate(`/conversations/${data.conversation_id}`),
+ },
+ )
}
>
{!isCreatingConversation && "Launch"}
diff --git a/frontend/src/components/features/home/tasks/task-card.tsx b/frontend/src/components/features/home/tasks/task-card.tsx
index 585376877c..7dfe4a3580 100644
--- a/frontend/src/components/features/home/tasks/task-card.tsx
+++ b/frontend/src/components/features/home/tasks/task-card.tsx
@@ -3,9 +3,7 @@ import { SuggestedTask } from "./task.types";
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { cn } from "#/utils/utils";
-import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { TaskIssueNumber } from "./task-issue-number";
-import { Provider } from "#/types/settings";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
const getTaskTypeMap = (
@@ -23,28 +21,19 @@ interface TaskCardProps {
export function TaskCard({ task }: TaskCardProps) {
const { setOptimisticUserMessage } = useOptimisticUserMessage();
- const { data: repositories } = useUserRepositories();
const { mutate: createConversation, isPending } = useCreateConversation();
const isCreatingConversation = useIsCreatingConversation();
const { t } = useTranslation();
- const getRepo = (repo: string, git_provider: Provider) => {
- const selectedRepo = repositories?.find(
- (repository) =>
- repository.full_name === repo &&
- repository.git_provider === git_provider,
- );
-
- return selectedRepo;
- };
-
const handleLaunchConversation = () => {
- const repo = getRepo(task.repo, task.git_provider);
setOptimisticUserMessage(t("TASK$ADDRESSING_TASK"));
return createConversation({
- selectedRepository: repo,
- suggested_task: task,
+ repository: {
+ name: task.repo,
+ gitProvider: task.git_provider,
+ },
+ suggestedTask: task,
});
};
diff --git a/frontend/src/components/features/settings/settings-dropdown-input.tsx b/frontend/src/components/features/settings/settings-dropdown-input.tsx
index 6514de1c0f..ba6b75db1c 100644
--- a/frontend/src/components/features/settings/settings-dropdown-input.tsx
+++ b/frontend/src/components/features/settings/settings-dropdown-input.tsx
@@ -1,5 +1,6 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
import { ReactNode } from "react";
+import { useTranslation } from "react-i18next";
import { OptionalTag } from "./optional-tag";
import { cn } from "#/utils/utils";
@@ -12,9 +13,12 @@ interface SettingsDropdownInputProps {
placeholder?: string;
showOptionalTag?: boolean;
isDisabled?: boolean;
+ isLoading?: boolean;
defaultSelectedKey?: string;
selectedKey?: string;
isClearable?: boolean;
+ allowsCustomValue?: boolean;
+ required?: boolean;
onSelectionChange?: (key: React.Key | null) => void;
onInputChange?: (value: string) => void;
defaultFilter?: (textValue: string, inputValue: string) => boolean;
@@ -29,13 +33,17 @@ export function SettingsDropdownInput({
placeholder,
showOptionalTag,
isDisabled,
+ isLoading,
defaultSelectedKey,
selectedKey,
isClearable,
+ allowsCustomValue,
+ required,
onSelectionChange,
onInputChange,
defaultFilter,
}: SettingsDropdownInputProps) {
+ const { t } = useTranslation();
return (