Mischief

58 / Agent UI

Stopped Run

What the thread says after an answer ended early, with whatever it had already written kept above the line.

The retry helper backs off between attempts, so the fix is to keep the delay and remove the listener before adding it again. The reconnect path currently registers

You stopped this answer. Ran for 7.4s.
You stopped this answer.

Installation

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

npx shadcn@latest add Tinkerers-Labs/mischief-ui/stopped-run
import { StoppedRun } from "mischief-ui/stopped-run"
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/stopped-run/stopped-run.tsx
"use client" import * as React from "react"import { CircleAlert, Hand, RotateCw, Timer } from "lucide-react" import { cn } from "@/lib/utils" export type StoppedReason = "stopped" | "error" | "limit" | "timeout" export type StoppedRunProps = Omit<  React.HTMLAttributes<HTMLDivElement>,  "children"> & {  reason?: StoppedReason

Usage

export function Ended({ text }) {
  return (
    <StoppedRun reason="stopped" elapsed={7.4} onRetry={again}>
      {text}
    </StoppedRun>
  )
}

Keep what it wrote

Clearing the half-written answer when a run ends is the tidy choice and the wrong one. Someone stopped it because they had read enough, and the part they read is usually the part they wanted.

So the text stays, faded at its bottom edge, with a rule and a sentence underneath saying it ended and why.

Half a sentence is not an answer

The fade says incomplete to someone who can see it and nothing at all to someone who cannot. A screen reader would otherwise read the fragment in the same voice as a finished answer and stop, which is how a truncated instruction becomes a followed one.

The partial text is a group named as an incomplete answer, so it is announced as such before it is read, and the reason is announced through a live region as soon as it appears.

Why it ended

ReasonWhat it says
stoppedYou stopped this answer
errorSomething went wrong
limitIt reached its length limit
timeoutIt took too long and was cut off

Carry on is offered only where carrying on makes sense, which is why it is a prop rather than something inferred: a stopped answer can be resumed and a failed one usually has to be run again.

API

reason"stopped" | "error" | "limit" | "timeout"Why it ended. Defaults to stopped.
childrenReactNodeWhat arrived before it ended.
messagestringReplaces the default sentence.
elapsednumberSeconds it ran for.
onRetry() => voidRun it again from the start.
onResume() => voidCarry on from where it stopped.
retryLabel, resumeLabelstringThe two buttons' text.
...rootPropsHTMLAttributes<HTMLDivElement>Native root attributes.

Accessibility

The partial answer is a group named as incomplete, so assistive technology says so before reading it rather than delivering a fragment in the voice of a finished answer. The reason is announced through a polite live region and is also written on the element as data-reason. The fade over the text is decoration and repeats nothing that is not in the sentence below it. Both actions are ordinary buttons at 32px with visible focus.