Mischief

16 / Agent UI

Message

One turn in a thread, with a role, an optional avatar and timestamp, and actions that stay reachable without a pointer.

You

Can you check the migration before I merge it?
2:14 PM

Assistant

The index is created before the backfill runs, so any row still holding a null email will collide. Reordering the two statements fixes it.
2:14 PM

Installation

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

npx shadcn@latest add Tinkerers-Labs/mischief-ui/message
import { Message } from "mischief-ui/message"

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

registry/default/message/message.tsx
"use client" import * as React from "react"import { cn } from "@/lib/utils" export type MessageRole = "user" | "assistant" | "system" export type MessageProps = React.HTMLAttributes<HTMLElement> & {  role: MessageRole  name?: React.ReactNode  avatar?: React.ReactNode  timestamp?: React.ReactNode  actions?: React.ReactNode  pending?: boolean

Usage

export function Turn() {
  return (
    <Message role="assistant" avatar="M" timestamp="just now">
      <StreamingText source={reply} />
    </Message>
  )
}

Avatars

The avatar slot takes whatever you give it and crops it into a 28 pixel circle. An image is scaled to fill and centred, so a portrait or a wide crop both work without letterboxing; initials or an icon work equally well, and are what to fall back to when someone has no picture.

<Message role="user" name="Aman" avatar={<img src={photo} alt="" />}>
  {text}
</Message>

<Message role="assistant" name="Mischief" avatar="M">
  {answer}
</Message>
Leave the image alt empty: the name beside it already says who this is.

The whole slot is hidden from assistive technology, because a picture of someone next to their name adds nothing to hear. That is also why an avatar alone is not enough to identify a speaker -- always pass name as well, or accept the role's default wording.

Roles and waiting

role sets the alignment, the tone, and the default name -- You, Assistant, or System. Override that with name whenever you have something better, which for an assistant is usually the product's own name rather than the word assistant.

pending marks a message that has been sent but not yet answered, or one still being written. Use it for the turn that is waiting rather than for one that failed: a message that will never arrive should say so in its own content, not sit pending forever.

actions is the row beneath the message, and is where Response Actions is designed to go.

API

role"user" | "assistant" | "system"Who is speaking. Sets the layout and the announced name.
nameReactNodeOverrides the name read out for the role.
avatarReactNodeInitials, an icon, or an img. An image is cropped to fill the circle whatever its shape.
timestampReactNodeShown under the body.
actionsReactNodeControls such as copy or regenerate.
pendingbooleanMarks the turn busy while it is still arriving.

Accessibility

Each turn is an article naming its speaker, so a thread can be navigated turn by turn instead of read as one block. The avatar is hidden from assistive technology, since the speaker is already named in text, so a profile picture needs no alternative text of its own. Actions are hidden with opacity rather than display, which keeps them focusable by keyboard and reveals them on focus as well as hover; on touch, where there is no hover, they stay visible. A turn still arriving reports aria-busy.