cruise 0.1.55

YAML-driven coding agent workflow orchestrator
Documentation
import { useEffect, useId, useRef, useState } from "react";
import { respondToAsk } from "../lib/commands";
import { ASK_USER_EVENT, type AskUserDetail } from "../lib/askUser";

interface AskUserDialogProps {
  /** Called with the session id after the user's answer is successfully submitted. */
  onAnswered?: (sessionId: string) => void;
}

type SubmitState =
  | { status: "idle" }
  | { status: "submitting" }
  | { status: "error"; message: string };

/**
 * Modal that prompts the user to answer an SDK `ask_user` question raised during
 * plan generation. Mounted once at the app root; it listens for the
 * {@link ASK_USER_EVENT} window event (dispatched from every PlanEvent handler)
 * so a single dialog serves the create / regenerate / fix flows.
 */
export function AskUserDialog({ onAnswered }: AskUserDialogProps) {
  const [pending, setPending] = useState<AskUserDetail | null>(null);
  const [answer, setAnswer] = useState("");
  const [submitState, setSubmitState] = useState<SubmitState>({ status: "idle" });
  const titleId = useId();
  const questionId = useId();
  // Monotonically-increasing nonce so consecutive ask_user calls within the
  // same session can be distinguished.  Without this, Q1's .then() would see
  // the same sessionId as Q2 and dismiss Q2's dialog.
  const askNonceRef = useRef(0);

  useEffect(() => {
    const handler = (e: Event) => {
      const detail = (e as CustomEvent<AskUserDetail>).detail;
      askNonceRef.current += 1;
      setPending(detail);
      setAnswer("");
      setSubmitState({ status: "idle" });
    };
    window.addEventListener(ASK_USER_EVENT, handler as EventListener);
    return () => window.removeEventListener(ASK_USER_EVENT, handler as EventListener);
  }, []);

  if (!pending) return null;

  const submitting = submitState.status === "submitting";

  const submit = () => {
    if (submitting) return;
    const { sessionId } = pending;
    const nonce = askNonceRef.current;
    setSubmitState({ status: "submitting" });
    respondToAsk(sessionId, answer)
      .then(() => {
        if (askNonceRef.current !== nonce) return;
        setPending(null);
        setAnswer("");
        onAnswered?.(sessionId);
      })
      .catch((e) => {
        if (askNonceRef.current !== nonce) return;
        // Keep the dialog open so the user can retry or see the failure reason.
        console.error("Failed to deliver ask_user answer:", e);
        setSubmitState({ status: "error", message: e instanceof Error ? e.message : String(e) });
      });
  };

  return (
    <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
      <div
        role="dialog"
        aria-modal="true"
        aria-labelledby={titleId}
        className="bg-gray-900 rounded-lg shadow-xl border border-gray-700 p-6 max-w-lg w-full space-y-4"
      >
        <h2 id={titleId} className="text-lg font-semibold text-gray-100">
          The planning agent has a question
        </h2>
        <p id={questionId} className="text-sm text-gray-300 whitespace-pre-wrap">
          {pending.question}
        </p>
        <textarea
          autoFocus
          aria-label="Your answer"
          aria-describedby={questionId}
          value={answer}
          onChange={(e) => setAnswer(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) submit();
          }}
          className="w-full h-28 border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 outline-none focus:border-blue-500 resize-none"
          placeholder="Your answer... (Cmd/Ctrl+Enter to send)"
          disabled={submitting}
        />
        {submitState.status === "error" && (
          <p className="text-sm text-red-400">{submitState.message}</p>
        )}
        <div className="flex justify-end">
          <button
            type="button"
            onClick={submit}
            disabled={submitting}
            className="px-4 py-1.5 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {submitting ? "Sending…" : "Send answer"}
          </button>
        </div>
      </div>
    </div>
  );
}