From 11cd91eef1c60775bd7df41d00ed1bd0f8e9e6ba Mon Sep 17 00:00:00 2001 From: Fabro Date: Mon, 4 May 2026 19:06:12 +0000 Subject: [PATCH] fabro(01KQT1TWWJYWZGDT8F05E29H9D): implement (succeeded) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fabro-Run: 01KQT1TWWJYWZGDT8F05E29H9D Fabro-Completed: 5 Fabro-Checkpoint: 05ae1ef055bf86466e6225bd3cb50f35e78878c5 ⚒️ Generated with [Fabro](https://fabro.sh) --- .../app/components/steer-composer.tsx | 157 ++++++ apps/fabro-web/app/lib/mutations.ts | 23 +- apps/fabro-web/app/lib/run-events.ts | 20 +- apps/fabro-web/app/routes/runs.tsx | 26 +- docs/public/api-reference/fabro-api.yaml | 85 ++- lib/crates/fabro-agent/src/lib.rs | 4 +- lib/crates/fabro-agent/src/session.rs | 487 +++++++++++++++--- lib/crates/fabro-agent/src/types.rs | 18 +- .../fabro-agent/tests/it/parity_matrix.rs | 6 +- lib/crates/fabro-cli/src/args.rs | 24 + lib/crates/fabro-cli/src/commands/run/mod.rs | 2 + .../fabro-cli/src/commands/run/runner.rs | 31 +- .../fabro-cli/src/commands/run/steer.rs | 32 ++ lib/crates/fabro-cli/tests/it/cmd/fabro.rs | 1 + lib/crates/fabro-client/src/client.rs | 21 + .../fabro-interview/src/control_protocol.rs | 52 ++ lib/crates/fabro-interview/src/lib.rs | 2 +- lib/crates/fabro-server/src/server.rs | 86 +++- .../fabro-server/src/server/handler/mod.rs | 3 +- .../fabro-server/src/server/handler/steer.rs | 126 +++++ lib/crates/fabro-server/src/server/tests.rs | 46 +- lib/crates/fabro-types/src/lib.rs | 2 + lib/crates/fabro-types/src/run_event/agent.rs | 26 + lib/crates/fabro-types/src/run_event/mod.rs | 16 + lib/crates/fabro-types/src/steering.rs | 52 ++ .../fabro-workflow/src/event/convert.rs | 18 +- lib/crates/fabro-workflow/src/event/events.rs | 44 +- lib/crates/fabro-workflow/src/event/names.rs | 4 + .../fabro-workflow/src/event/stored_fields.rs | 37 +- .../fabro-workflow/src/handler/llm/api.rs | 91 +++- lib/crates/fabro-workflow/src/lib.rs | 2 + .../fabro-workflow/src/operations/start.rs | 10 + .../src/pipeline/execute/tests.rs | 9 +- .../fabro-workflow/src/pipeline/initialize.rs | 150 +++--- .../fabro-workflow/src/pipeline/types.rs | 2 + lib/crates/fabro-workflow/src/steering_hub.rs | 337 ++++++++++++ .../fabro-api-client/src/models/index.ts | 3 +- .../src/models/steer-run-request.ts | 29 ++ 38 files changed, 1923 insertions(+), 161 deletions(-) create mode 100644 apps/fabro-web/app/components/steer-composer.tsx create mode 100644 lib/crates/fabro-cli/src/commands/run/steer.rs create mode 100644 lib/crates/fabro-server/src/server/handler/steer.rs create mode 100644 lib/crates/fabro-types/src/steering.rs create mode 100644 lib/crates/fabro-workflow/src/steering_hub.rs create mode 100644 lib/packages/fabro-api-client/src/models/steer-run-request.ts 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..e27c8c77b --- /dev/null +++ b/apps/fabro-web/app/components/steer-composer.tsx @@ -0,0 +1,157 @@ +import { useEffect, useRef, useState } from "react"; + +import { ApiError } from "../lib/api-client"; +import { useSteerRun } from "../lib/mutations"; + +interface SteerComposerProps { + runId: string; + open: boolean; + onClose: () => void; +} + +/** + * Modal composer for sending a mid-run steering message to a running run. + * + * Renders a textarea + two action buttons: + * - Send: appends to the steering queue (default). + * - Interrupt: cancels the in-flight LLM stream / tool calls and delivers + * the message as the next user turn. + * + * Surfaces 409 errors inline so users see the rejection reason (e.g., the + * `cli_agent_not_steerable` code). + */ +export function SteerComposer({ runId, open, onClose }: SteerComposerProps) { + const [text, setText] = useState(""); + const [errorMessage, setErrorMessage] = useState(null); + const textareaRef = useRef(null); + const { trigger, isMutating } = useSteerRun(runId); + + // Autofocus when opening; reset state when closing. + useEffect(() => { + if (open) { + requestAnimationFrame(() => textareaRef.current?.focus()); + } else { + setText(""); + setErrorMessage(null); + } + }, [open]); + + // Close on Escape. + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, onClose]); + + 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
+