Skip to content
KONIGI

AI Assistants / Turn and response / Message turn

2 of 6

Message turn

Two voices alternate down one column, and the reader has to know at a glance which one is speaking.

Updated September 12, 2026

Problem

A transcript is one column with two speakers in it. The reader scrolling back through yesterday’s conversation needs to find the moment they asked a particular thing, and they need to do it without reading. Undifferentiated paragraphs make that impossible.

Solution

The instinct inherited from messaging is a bubble on each side, left and right. It fails here for a structural reason. In SMS both parties write roughly the same amount, so symmetric treatment is honest. In an assistant transcript the question is eleven words and the answer is nine hundred, with headings, a table and a code block in it. A bubble drawn around nine hundred words of formatted prose constrains the measure, breaks the code block, and decorates the thing the reader came for.

So the field converged on asymmetry. The user turn gets the chrome: a tint, a rounded container, a constrained width. The assistant turn gets none, and runs as plain prose at the full column measure. This reads as a reversal of importance and is the opposite. The assistant turn is the content. The user turn is the label on it, and a label should be small, distinct and scannable.

That also matches how people actually navigate a long conversation. They look for their own questions, because a question is eleven words they wrote and remember. The answers are found by way of the questions. Making user turns compact and visually distinct turns them into landmarks down the left edge, which is worth more than making them pretty.

Attribution beyond that’s mostly removable. Once the asymmetry carries the signal, the assistant avatar is a 40-pixel gutter repeated down every turn forever and paying for nothing. Timestamps are a stronger habit and a weaker idea: messaging needs them because humans reply hours apart, and in an assistant transcript both turns happened four seconds apart. Put the time on the conversation—not the turn.

The semantics have to be built rather than implied, because colour and indentation reach nobody using a screen reader. The transcript wants list semantics with each turn as an item, and every turn wants a programmatic speaker label that doesn’t depend on its styling. Alignment-based attribution is the most fragile version of all of this, since it carries no meaning to assistive technology and inverts under right-to-left text.

Use when

Any interface where a person and a model take alternating turns and the history stays on screen.

Don’t use when

The exchange is one question and one answer with no history, which is how answer engines and most in-product assists work. There turn chrome is pure decoration, and the answer reads better as a document with the question as its heading.

Trade-offs

Symmetric bubbles cap the measure at something too narrow for code and tables. Dropping chrome from both sides loses the speaker signal entirely and produces a wall. Alternating background tints seem like a cheap fix and produce visible banding down a long page, which gets worse as turns get shorter. And compact user turns collide with long pasted input: someone drops in a two-thousand-word document to summarise, and the landmark becomes the biggest object on the page. Collapsing long user turns to a few lines with an expander is the usual answer, and it needs to be obvious that text was hidden.

Checklist

  • Can the reader find their own question from six turns ago without reading any answers?
  • Does the assistant turn run at the full measure, or is it boxed?
  • What does a two-thousand-word pasted user turn do to the layout?
  • Is the speaker exposed programmatically, or only through colour and position?
  • Does the attribution survive right-to-left text?
  • What does the avatar or the timestamp earn on every single turn?
  • How does a code block inside an assistant turn behave at the narrowest supported width?
  • Is there banding down the page once turns get short?
  • Where does a system message or an error sit in the two-voice scheme?
  • Can a single turn be linked to, quoted, or shared on its own?

Compare

ChatGPT tints and constrains the user turn and lets the assistant turn run full width with no avatar. That’s the asymmetry in its most common form. Claude does the same and pushes it further by moving long generated documents out of the transcript entirely, so the turn stays a turn. Perplexity drops turn chrome almost completely: the question becomes a heading and the answer is a document underneath it, which is the right call for one-shot use and loses the thread once a conversation actually develops. Slack takes the opposite trade, because an AI reply lands in the existing message list and inherits the same avatar, name and timestamp row every human message gets. The assistant becomes a participant rather than a surface.

Streaming response is what fills an assistant turn while the reader watches. Response actions attach to the turn and have to sit somewhere that doesn’t compete with it. Conversation history is the layer above, where turns become a thread with a name. Edit and resend rewrites a user turn in place rather than appending another one. Composer produces the turns on one side of the exchange.

Message turn anatomy Two turns in one column. The user turn is a compact tinted container acting as a landmark; the assistant turn runs at the full measure with no container and no avatar. A long pasted user turn is collapsed to a few lines with an expander. Asymmetry, because the two turns are different sizes Summarise the attached lease Show all 247 words no avatar, no timestamp 1 2 3 4 1 THE USER TURN Compact and tinted. It is the label on the answer, and the landmark people scan. 2 THE ASSISTANT TURN Full measure, no container. It is the content, so nothing decorates it. 3 WHAT WAS REMOVED An avatar is a gutter repeated forever. Both turns happened four seconds apart. 4 THE LONG PASTE Two thousand pasted words make the landmark the biggest thing on the page. Colour and position carry none of this to a screen reader. The speaker has to be in the markup.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Two turns, deliberately asymmetric. The user turn is a compact tinted container acting as a landmark; the assistant turn runs at full measure with no container and no avatar, because it is the thing being read.

shadcn
npx shadcn@latest add button
Tokens
--foreground--card--muted--border--chart-1
  1. You: Summarise the attached lease
  2. Assistant: The lease runs five years from 1 March 2024 at £42,000 a year, reviewed upward only at the start of year three. There is a tenant-only break at the end of year two on six months' written notice, conditional on the rent being paid up to date and vacant possession being given. The deposit is three months' rent, held in a separate account and returned within 30 days of the end of the term, less any agreed dilapidations.
  3. You: Also check this against what you said. 12. BREAK CLAUSE 12.1 The Tenant may determine this Lease on the Break Date by giving the Landlord not less than six months' prior written notice. 12.2 The Lease will determine on the Break Date only if the Tenant has paid all Annual Rent due up to and including the Break Date, and gives vacant possession of the Premises on the Break Date. 12.3 If the Lease determines under this clause the Landlord shall refund to the Tenant, within 14 days, any Annual Rent paid in advance for the period after the Break Date. 13. RENT REVIEW 13.1 The Annual Rent shall be reviewed on the Review Date to the greater of the Annual Rent payable immediately before the Review Date and the Open Market Rent. 13.2 The Open Market Rent shall be agreed between the parties or, in default of agreement within two months before the Review Date, determined by an independent surveyor acting as an expert. 14. DEPOSIT 14.1 The Tenant shall on the date of this Lease pay to the Landlord the Deposit, being a sum equal to three months' Annual Rent, to be held in a separate interest-bearing account. 14.2 The Landlord may draw on the Deposit to make good any breach of the Tenant's covenants. 14.3 The Landlord shall return the balance of the Deposit to the Tenant within 30 days after the end of the Term, less any sums properly deducted under clause 14.2.

MessageTurn.tsxA list of turns, each with its speaker in the markup. A user turn past 120 words collapses to two lines.

import { useState } from "react";
import { Button } from "@/components/ui/button";

export type Turn = { role: "user" | "assistant"; text: string };

/** A user turn past this many words is a paste, and a paste collapses. Two
 *  thousand pasted words otherwise make the landmark the biggest thing on
 *  the page. */
const COLLAPSE_OVER = 120;

/**
 * The transcript is a list and every turn is an item with a speaker in the
 * markup. Tint and alignment carry none of this to a screen reader, and
 * alignment inverts under right-to-left text.
 */
export function Transcript({ turns }: { turns: Turn[] }) {
  return (
    <ol className="flex flex-col gap-4">
      {turns.map((t, i) => (
        <li key={i}><MessageTurn {...t} /></li>
      ))}
    </ol>
  );
}

export function MessageTurn({ role, text }: Turn) {
  const [open, setOpen] = useState(false);

  // The assistant turn is the content, so nothing decorates it: no container,
  // no avatar, no timestamp, the full measure. In practice this is rendered
  // markdown; the point is what wraps it, which is nothing.
  if (role === "assistant") {
    return (
      <div className="text-sm leading-relaxed text-foreground">
        <span className="sr-only">Assistant: </span>
        {text}
      </div>
    );
  }

  const words = text.trim().split(/\s+/).length;
  const collapsed = words > COLLAPSE_OVER && !open;

  // The user turn is the label on the answer and the landmark people scan
  // for, so it is compact, tinted and narrow.
  return (
    <div className="flex flex-col items-end gap-1.5">
      <div className="max-w-md rounded-xl bg-muted px-3.5 py-2 text-sm text-foreground">
        <span className="sr-only">You: </span>
        <span className={collapsed ? "line-clamp-2" : undefined}>{text}</span>
      </div>
      {collapsed && (
        <Button variant="link" size="sm" className="h-auto p-0 text-xs text-chart-1" onClick={() => setOpen(true)}>
          Show all {words.toLocaleString()} words
        </Button>
      )}
    </div>
  );
}

demo.tsxHow it is called: three turns, the third a paste long enough to collapse.

import { Transcript } from "./MessageTurn";

const PASTE =
  "Also check this against what you said. 12. BREAK CLAUSE 12.1 The Tenant may determine this Lease on the Break Date by giving the Landlord not less than six months' prior written notice. 12.2 The Lease will determine on the Break Date only if the Tenant has paid all Annual Rent due up to and including the Break Date, and gives vacant possession of the Premises on the Break Date. 12.3 If the Lease determines under this clause the Landlord shall refund to the Tenant, within 14 days, any Annual Rent paid in advance for the period after the Break Date. 13. RENT REVIEW 13.1 The Annual Rent shall be reviewed on the Review Date to the greater of the Annual Rent payable immediately before the Review Date and the Open Market Rent. 13.2 The Open Market Rent shall be agreed between the parties or, in default of agreement within two months before the Review Date, determined by an independent surveyor acting as an expert. 14. DEPOSIT 14.1 The Tenant shall on the date of this Lease pay to the Landlord the Deposit, being a sum equal to three months' Annual Rent, to be held in a separate interest-bearing account. 14.2 The Landlord may draw on the Deposit to make good any breach of the Tenant's covenants. 14.3 The Landlord shall return the balance of the Deposit to the Tenant within 30 days after the end of the Term, less any sums properly deducted under clause 14.2.";

/** Three turns: a question, the answer at full measure, and a paste that collapses. */
export default function Demo() {
  return (
    <div className="rounded-lg border bg-card p-4">
      <Transcript
        turns={[
          { role: "user", text: "Summarise the attached lease" },
          {
            role: "assistant",
            text: "The lease runs five years from 1 March 2024 at £42,000 a year, reviewed upward only at the start of year three. There is a tenant-only break at the end of year two on six months' written notice, conditional on the rent being paid up to date and vacant possession being given. The deposit is three months' rent, held in a separate account and returned within 30 days of the end of the term, less any agreed dilapidations.",
          },
          { role: "user", text: PASTE },
        ]}
      />
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.