Mischief

47 / Agent UI

Stop Generating

The control that interrupts a running answer, with the time it has been going and Escape wired up.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/stop-generating
import { StopGenerating } from "mischief-ui/stop-generating"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/stop-generating/stop-generating.tsx
"use client" import * as React from "react"import { Square } from "lucide-react" import { cn } from "@/lib/utils" export type StopGeneratingProps = Omit<  React.ButtonHTMLAttributes<HTMLButtonElement>,  "children" | "onClick"> & {  onStop: () => void  /** Renders nothing while false. Defaults to true. */  running?: boolean

Usage

export function Composer() {
  return (
    <StopGenerating
      running={streaming}
      startedAt={startedAt}
      onStop={abort}
    />
  )
}

Where it belongs

Put it where the send control was. Someone who has just started an answer is still looking at that spot, and a stop button somewhere else costs them a search at exactly the moment they want it to be over. Prompt Input does this for you by swapping its own control while streaming.

It renders nothing when there is nothing to stop, rather than dimming. A disabled stop button is a small lie: it suggests the option exists and is unavailable, when in fact there is simply no work in flight.

Escape, and the promise it makes

Escape is bound while running and unbound the moment it stops, so the key never quietly does something on a page where nothing is happening. Turn it off with shortcut={false} where Escape already belongs to something else -- a dialog holding the composer, for instance, which should close rather than interrupt.

Whatever onStop does, it should genuinely stop: abort the request, not just hide the text. A stop that only stops the display leaves the model running, the bill accruing, and the answer arriving anyway if the component remounts.

const controller = useRef<AbortController>(null)

<StopGenerating
  running={streaming}
  startedAt={startedAt}
  onStop={() => controller.current?.abort()}
/>

API

onStop() => voidCalled on click, and on Escape while the shortcut is on.
runningbooleanRenders nothing while false. Defaults to true.
startedAtnumberEpoch milliseconds. Drives the live elapsed reading.
showElapsedbooleanShows the elapsed seconds. Defaults to true.
shortcutbooleanBinds Escape while running. Defaults to true.
labelstringNames the control. Defaults to "Stop generating".

Accessibility

The control disappears rather than dimming when there is nothing to stop, so it is never a button that does nothing. Escape is bound only while running and unbound as soon as it stops. The latest handler is read through a ref, so a changing callback never rebinds the key or leaves a stale one behind. The elapsed reading is decoration beside the name, not the name itself.