Mischief

85 / Agent UI

Presence Field

An ambient backdrop that carries what the assistant is doing. It changes colour and pace with the state, and settles into each one rather than snapping to it.

thinking

The room changes with the assistant, and settles rather than snaps.

Installation

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

npx shadcn@latest add Tinkerers-Labs/mischief-ui/presence-field
import { PresenceField } from "mischief-ui/presence-field"

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

registry/default/presence-field/presence-field.tsx
"use client" import * as React from "react"import {  RenderSurface,  createQuadProgram,  useThemeColors,  type QuadProgram,  type SurfaceColor,} from "@/registry/default/render-surface/render-surface"import { cn } from "@/lib/utils" export type AgentPresence = "idle" | "thinking" | "streaming" | "done" | "error" 

Usage

export function Thread({ status }) {
  return (
    <PresenceField state={status} className="rounded-xl">
      <Conversation>{/* messages */}</Conversation>
      <ThinkingState status={status} />
    </PresenceField>
  )
}

A second channel, never the only one

This is the one rule that matters here. A colour behind a thread is not a status: it cannot be read out, it is invisible to anyone who cannot distinguish the two colours you chose, and it says nothing at all to a reader who has motion turned off.

Put it behind a thread whose state is already written down. The thinking state component says what is happening in words; this says the same thing in the room around it. Take the words away and you have a page that changes colour for no stated reason.

<PresenceField state={status}>
  <Conversation>{messages}</Conversation>
  <ThinkingState status={status} />
</PresenceField>
The field decorates the state. It does not report it.

It settles rather than switches

Both the colour and the pace are eased toward whatever the current state calls for, on every frame, rather than being set when the state changes. A thread that finishes drifts down to rest over about a second instead of cutting to a new colour.

That easing is why the states are not simply four different shaders. There is one field, and the state moves it.

StatePaceColour
idleVery slowThe quiet colour
thinkingSteadyThe active colour
streamingQuickest, and quicker again with activityThe active colour
doneAlmost stillThe active colour, faint
errorUnsettledThe fault colour

Keeping the middle quiet

The field is brightest at the edges and weakest in the middle, because the middle is where the thread is. Text stays on an almost plain background while the movement happens around it.

API

state"idle" | "thinking" | "streaming" | "done" | "error"What the assistant is doing. Defaults to "idle".
basestringThe colour it settles to. Defaults to "--background".
activestringWhile it is working. Defaults to "--primary".
faultstringWhen something went wrong. Defaults to "--destructive".
quietstringWhile it is idle. Defaults to "--muted-foreground".
activitynumberNought to one, for how much is arriving. Only read while streaming.

Accessibility

The canvas is decoration and is hidden from assistive technology, deliberately: the state belongs to the component that states it in words. Under reduced motion one frame is drawn and the field never moves, which is exactly why it must not be the only signal. Children are ordinary markup above it.