From b2c751caca5d5484ccf8d4e6e139b83d53d51a2e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 7 Mar 2026 14:32:12 -0600 Subject: [PATCH] feat(web): MS23-P2-003 BargeInInput component --- .../mission-control/BargeInInput.tsx | 147 ++++++++++++++++++ .../mission-control/MissionControlLayout.tsx | 2 +- .../mission-control/OrchestratorPanel.tsx | 66 ++++---- 3 files changed, 184 insertions(+), 31 deletions(-) create mode 100644 apps/web/src/components/mission-control/BargeInInput.tsx diff --git a/apps/web/src/components/mission-control/BargeInInput.tsx b/apps/web/src/components/mission-control/BargeInInput.tsx new file mode 100644 index 0000000..d83c735 --- /dev/null +++ b/apps/web/src/components/mission-control/BargeInInput.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useCallback, useState, type KeyboardEvent } from "react"; +import { Loader2 } from "lucide-react"; +import { useToast } from "@mosaic/ui"; +import { Button } from "@/components/ui/button"; +import { apiPost } from "@/lib/api/client"; + +const MAX_ROWS = 4; +const TEXTAREA_MAX_HEIGHT_REM = 6.5; + +interface BargeInMutationResponse { + message?: string; +} + +export interface BargeInInputProps { + sessionId: string; + onSent?: () => void; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + return "Failed to send message to the session."; +} + +export function BargeInInput({ sessionId, onSent }: BargeInInputProps): React.JSX.Element { + const { showToast } = useToast(); + const [content, setContent] = useState(""); + const [pauseBeforeSend, setPauseBeforeSend] = useState(false); + const [isSending, setIsSending] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const handleSend = useCallback(async (): Promise => { + const trimmedContent = content.trim(); + if (!trimmedContent || isSending) { + return; + } + + const encodedSessionId = encodeURIComponent(sessionId); + const baseEndpoint = `/api/mission-control/sessions/${encodedSessionId}`; + let didPause = false; + let didInject = false; + + setIsSending(true); + setErrorMessage(null); + + try { + if (pauseBeforeSend) { + await apiPost(`${baseEndpoint}/pause`); + didPause = true; + } + + await apiPost(`${baseEndpoint}/inject`, { content: trimmedContent }); + didInject = true; + setContent(""); + onSent?.(); + } catch (error) { + const message = getErrorMessage(error); + setErrorMessage(message); + showToast(message, "error"); + } finally { + if (didPause) { + try { + await apiPost(`${baseEndpoint}/resume`); + } catch (resumeError) { + const resumeMessage = getErrorMessage(resumeError); + const message = didInject + ? `Message sent, but failed to resume session: ${resumeMessage}` + : `Failed to resume session: ${resumeMessage}`; + setErrorMessage(message); + showToast(message, "error"); + } + } + + setIsSending(false); + } + }, [content, isSending, onSent, pauseBeforeSend, sessionId, showToast]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent): void => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + void handleSend(); + } + }, + [handleSend] + ); + + const isSendDisabled = isSending || content.trim().length === 0; + + return ( +
+