diff --git a/apps/fabro-web/app/components/steer-composer.tsx b/apps/fabro-web/app/components/steer-composer.tsx new file mode 100644 index 000000000..4c5156a35 --- /dev/null +++ b/apps/fabro-web/app/components/steer-composer.tsx @@ -0,0 +1,143 @@ +import { useEffect, useRef, useState } from "react"; + +import { ApiError } from "../lib/api-client"; +import { useSteerRun } from "../lib/mutations"; +import { ErrorMessage } from "./ui"; + +interface SteerComposerProps { + runId: string; + open: boolean; + onClose: () => void; +} + +export function SteerComposer({ runId, open, onClose }: SteerComposerProps) { + const [text, setText] = useState(""); + const [errorMessage, setErrorMessage] = useState(null); + const textareaRef = useRef(null); + const onCloseRef = useRef(onClose); + onCloseRef.current = onClose; + const { trigger, isMutating } = useSteerRun(runId); + + useEffect(() => { + if (open) { + requestAnimationFrame(() => textareaRef.current?.focus()); + } else { + setText(""); + setErrorMessage(null); + } + }, [open]); + + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onCloseRef.current(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open]); + + if (!open) return null; + + const trimmed = text.trim(); + const canSubmit = trimmed.length > 0 && !isMutating; + + async function send(interrupt: boolean) { + if (!canSubmit) return; + setErrorMessage(null); + try { + await trigger({ text: trimmed, interrupt }); + onClose(); + } catch (err) { + if (err instanceof ApiError) { + // Try to surface the well-known 409 codes inline. + const body = err.body as { code?: string; detail?: string } | null; + if (body?.code === "cli_agent_not_steerable") { + setErrorMessage( + "All running agent stages are CLI-mode and can't be steered.", + ); + } else if (body?.code === "use_answer_endpoint") { + setErrorMessage( + "Run is blocked on a question; answer the question first.", + ); + } else { + setErrorMessage(body?.detail ?? err.message ?? "Steer failed."); + } + } else { + setErrorMessage("Steer failed; try again."); + } + } + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + void send(false); + } + } + + return ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
+
Steer agent
+