feat(frontend): pulse the stop dot while the harness is working

Give live feedback that something is happening the moment a session starts
processing: the red stop dot in the composer beats with a soft heartbeat + halo
while the harness is actively working (running — not waiting at a gate or
finalized), and settles the instant the model hands control back to the reviewer
or the session ends. Ring/scale only (no layout properties animated); honors
prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:33:25 +02:00
co-authored by Claude Opus 4.8
parent 3453f3ae23
commit ccfa3a18bf
4 changed files with 31 additions and 1 deletions
+1
View File
@@ -508,6 +508,7 @@ export function AppShell() {
onStop={() => setStopConfirm(true)}
inputRef={composerRef}
awaitingInput={awaitingQuestion || pendingWidget?.widget === "freetext"}
working={running}
/>
</div>
<ComposerFooter />
+9
View File
@@ -82,3 +82,12 @@ test("marks the composer as awaiting input only when requested", () => {
expect(input).toHaveAttribute("data-awaiting-input", "true");
expect(input).toHaveClass("thot-awaiting-input");
});
test("pulses the stop dot only while the harness is working", () => {
const { rerender } = render(<SteerInput sessionId="s1" working />);
const dot = () =>
screen.getByRole("button", { name: /stop and save session/i }).querySelector("span");
expect(dot()).toHaveClass("thot-pulse");
rerender(<SteerInput sessionId="s1" working={false} />);
expect(dot()).not.toHaveClass("thot-pulse");
});
+7 -1
View File
@@ -26,6 +26,8 @@ interface Props {
inputRef?: React.Ref<HTMLTextAreaElement>;
/** The workflow is explicitly waiting for text in this composer. */
awaitingInput?: boolean;
/** The harness is actively working: pulse the stop dot to signal live activity. */
working?: boolean;
}
export function SteerInput({
@@ -36,6 +38,7 @@ export function SteerInput({
onStop,
inputRef,
awaitingInput = false,
working = false,
}: Props) {
const [text, setText] = useState("");
const [busy, setBusy] = useState(false);
@@ -115,7 +118,10 @@ export function SteerInput({
aria-label="Stop and save session"
className="grid size-7 shrink-0 place-items-center rounded-md border border-border text-primary transition-colors hover:bg-muted"
>
<span className="block size-2.5 rounded-full bg-primary" />
<span
className={["block size-2.5 rounded-full bg-primary", working && "thot-pulse"]
.filter(Boolean).join(" ")}
/>
</button>
)}
<textarea