import { useEffect, useRef, useState, type FormEvent } from "react"; import type { CapabilityComposerModel } from "../../../src/issue-thread/types"; /** * Composer (contract §4). `data-composer-state` is the single test and * screenshot hook. Draft text survives refresh through localStorage keyed by * session id; Stop never discards the transcript. */ export interface ComposerProps { model: CapabilityComposerModel; sessionId: string; onSend: (message: string) => void; onStop: () => void; onRetry: () => void; onReset: () => void; onFocusPending: (interactionId: string) => void; /** The clean room resets into a new tenant, so it names the action itself. */ resetLabel?: string; } function draftKey(sessionId: string): string { return `paperclip-runner.capability.draft.${sessionId}`; } function readDraft(sessionId: string): string { try { return window.localStorage.getItem(draftKey(sessionId)) ?? ""; } catch { return ""; } } export function Composer(props: ComposerProps) { const { model, sessionId, onSend, onStop, onRetry, onReset, onFocusPending, resetLabel = "Reset scenario", } = props; const [value, setValue] = useState(() => readDraft(sessionId)); const inputRef = useRef(null); useEffect(() => { setValue(readDraft(sessionId)); }, [sessionId]); useEffect(() => { try { window.localStorage.setItem(draftKey(sessionId), value); } catch { // A blocked storage quota must not break the session. } }, [sessionId, value]); const editable = model.state === "ready" || model.state === "streaming"; const canSend = editable && value.trim().length > 0; function submit(event: FormEvent) { event.preventDefault(); if (!canSend) return; onSend(value.trim()); setValue(""); } return (
{model.helper !== null ? (

{model.helper} {model.state === "waiting" && model.pendingInteractionId !== null ? ( <> {" "} ) : null}

) : null} {model.reason !== null ? (

{model.reason}

) : null}