Skip to content
Tier

Turn request

Pro

The agent asks the person to take the screen: for a sign-in, a one-time code, a check it must not pass itself. It says why, what to do, what the app says the agent won't do meanwhile, and whether the agent can still see the screen while the person has it, or that the app hasn't said. Take the screen, an optional quieter alternative, and Not now are offered side by side: none is focused, pre-selected or drawn as the default. After the person hands the screen back, the app passes the agent's report of what it saw, shown as the agent's words, not checked.

The app must stop the agent's input while the person has the screen: Candor sends no input and can't enforce wontDo, which is the app's commitment, drawn as the app gives it. It composes with AgentScreen through the app: on onTake, set AgentScreen's control="person", and call its onHandBack as usual. variant="strip" draws it as a band to sit in AgentScreen's place or above it.

With taken, the app says the person has the screen now: it reads "You have the screen. Hand it back when you’re done." with Hand back (onHandBack), keeps what the agent won't do and whether it sees the screen in view, as they matter most while the person has it, and locks Take the screen, saying why. Taken is the app's word, never inferred from a press of Take: the app knows when the screen is really the person's.

When the request changes (another one, or the report after hand-back) and focus was in it, focus goes to its heading; when focus is elsewhere, the new request's title or the report's arrival is announced once, while it is shown. A request is told from the last by its words (why and todo) and the agent asking: there is no id to go by, and new words are a new request to the person reading them. Nothing is announced on mount.

Markdown
Version 5.4.0· other versions

This page is for 5.4.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0, 5.0.0, 5.1.0 and 5.2.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/turn-request

Brings in: agent-avatar, bidi, button, chat, heading-level, heading-scope, locale, locked, tokens, use-announcer, use-focus-within, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import type { AgentIdentity } from "@/lib/candor/chat"
import { AgentScreen } from "@/components/candor/agent-screen"
import { TurnRequest } from "@/components/candor/turn-request"

const agent: AgentIdentity = { id: "a-web", name: "Browser agent", role: "dev" }

// A drawn stand-in for the screenshot your server sends: a sign-in page asking for a one-time code.
const frame =
  "data:image/svg+xml," +
  encodeURIComponent(
    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 400"><rect width="640" height="400" fill="#f6f6f4"/><text x="200" y="130" font-family="sans-serif" font-size="22" fill="#333">Enter your one-time code</text><rect x="200" y="160" width="240" height="40" rx="4" fill="#fff" stroke="#ccc"/><rect x="200" y="220" width="120" height="36" rx="4" fill="#3b6fd8"/><text x="236" y="244" font-family="sans-serif" font-size="15" fill="#fff">Verify</text></svg>`
  )

export default function Example() {
  // "asking", then "person" while they have the screen, then "back" once they hand it back; or "link" when they asked
  // for a sign-in link instead, or "declined".
  const [phase, setPhase] = React.useState<"asking" | "declined" | "link" | "person" | "back">("asking")
  // While the person has the screen (`taken`), or a link is on its way, the request stays where it is with its buttons
  // locked and saying why, so focus stays on the button pressed instead of falling to the page. With `taken`, TurnRequest
  // locks Take the screen itself; the other two are this app's to lock.
  const busy = phase === "person" ? "You have the screen now." : phase === "link" ? "A sign-in link is on its way to your phone." : null
  // After Not now, a line in the request's place says what happens next and takes focus, so focus never falls to the
  // page when the button pressed goes.
  const declined = React.useRef<HTMLParagraphElement>(null)
  React.useEffect(() => {
    if (phase === "declined") declined.current?.focus()
  }, [phase])

  return (
    <div className="flex max-w-3xl flex-col gap-4">
      {phase === "declined" ? (
        <p ref={declined} tabIndex={-1} className="rounded-xl border bg-card p-4 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80">
          Not now. Browser agent will ask again when it next reaches the code page.
        </p>
      ) : (
        <TurnRequest
          agent={agent}
          why="The supplier's site asks for a one-time code sent to your phone."
          todo="Enter the code and press Verify, then hand the screen back."
          // Your app's commitment: it must stop the agent's input while you have the screen. Candor sends none.
          wontDo={["Type or click anything", "Keep a copy of what you type"]}
          seesScreen={false}
          onTake={() => setPhase("person")}
          takeLocked={phase === "link" ? busy : null}
          // Your app says when the screen is the person's: here, as soon as it has given them control.
          taken={phase === "person"}
          onHandBack={() => setPhase("back")}
          onNotNow={() => setPhase("declined")}
          notNowLocked={busy}
          // The quieter way keeps the code on the person's own device: the agent never handles it.
          alternative={{ label: "Send me a sign-in link instead", why: "You open the link and sign in on your own device; the agent waits.", onChoose: () => setPhase("link") }}
          alternativeLocked={busy}
          report={phase === "back" ? "The code was accepted and the supplier's order page is open." : undefined}
        />
      )}
      <AgentScreen
        agent={agent}
        connection="live"
        source={{ kind: "image", src: frame }}
        action={phase === "person" ? undefined : "Waiting at the one-time code page"}
        control={phase === "person" ? "person" : "agent"}
        controlStyle="strip"
        waiting={phase === "asking" ? "enter the one-time code" : undefined}
        onHandBack={() => setPhase("back")}
      />
    </div>
  )
}

<TurnRequest>

PropTypeDefaultDescription
agentrequiredAgentIdentity—The agent asking, named by the app.
whyrequiredstring—Why the agent asks, in the app's or the agent's words: "The supplier's site wants a one-time code." Shown as text. A change to it is a new request: with focus elsewhere, the title is announced again. Pass it whole rather than streaming it in, or each change is announced.
todorequiredstring—What the person is asked to do, in the app's words: "Sign in, then hand the screen back." Shown as text. As with `why`, a change is a new request, announced by the title when focus is elsewhere: pass it whole.
wontDoreadonly string[] | undefined—What the agent won't do while the person has the screen, in the app's words: "Read what you type", "Click anything". The app's commitment: the app must hold to it, as Candor can't. Left out or empty, it says the app hasn't said.
seesScreenboolean | undefined—Whether the agent can still see the screen while the person has it (and so sees what they type). Left out, it reads "The app hasn’t said whether {agent} can see the screen while you have it."
onTake(() => void) | undefined—Shows Take the screen; called when it's pressed. Set AgentScreen's `control="person"` from here.
takeLockedstring | null | undefined—Why the person can't take the screen, if they can't. The button stays and says so. `null` or unset means they can.
onNotNow(() => void) | undefined—Shows Not now; called when it's pressed.
notNowLockedstring | null | undefined—Why the person can't decline, if they can't. `null` or unset means they can.
alternativeTurnRequestAlternative | undefined—A quieter way, offered beside taking the screen, with why someone might choose it.
alternativeLockedstring | null | undefined—Why the alternative can't be chosen, if it can't. `null` or unset means it can.
reportstring | undefined—After the person hands the screen back: the agent's account of what it saw, as text. With it, the request is over: the actions go and the report shows, named as the agent's and not checked.
takenboolean | undefined—The app says the person has the screen now. It draws "You have the screen. Hand it back when you’re done." with Hand back (`onHandBack`), keeps the `wontDo` and `seesScreen` lines in view, and locks Take the screen, saying why. It is the app's statement, never inferred from a press of Take: pass it once the screen is the person's. Focus stays where it was when it turns true; when it turns false with no `report` and focus was in the request (on Hand back, say), focus goes to the request's heading. With `report`, the report shows instead. Left out, nothing changes.
onHandBack(() => void) | undefined—Shows Hand back while `taken`; called when it's pressed. Hand the screen back to the agent here, as AgentScreen's `onHandBack` does, and pass the agent's `report` when it has one.
variant"card" | "strip" | undefined"card""card" (the default), or "strip": a denser band to sit in AgentScreen's place or above it.
classNamestring | undefined—Extra classes for the outer element.
headingLevelHeadingLevel | undefined—The level of its title. Defaults to one below the nearest `HeadingScope`, else 2.

Strings

The fixed words, in English by default. Change any of them under turnRequest in <LocaleProvider strings={{ … }}>; the table is exported as turnRequestStrings.

KeyEnglish
title{agent} asks you to take the screen
whyWhy
todoWhat to do
wontDoWhat the app says {agent} won’t do meanwhile
wontDoUnknownThe app hasn’t said what {agent} won’t do while you have the screen.
seesScreen{agent} can still see the screen while you have it.
seesScreenNot{agent} can’t see the screen while you have it.
seesScreenUnknownThe app hasn’t said whether {agent} can see the screen while you have it.
takeTake the screen
notNowNot now
backThe screen is back with {agent}
reportWhat {agent} says it saw
reportNote{agent}’s own account, not checked.
takenYou have the screen. Hand it back when you’re done.
takenLockedYou have the screen already.
handBackHand back to {agent}

Types

TurnRequestAlternative
A quieter way to do what the agent asks, offered beside taking the screen: its label, why, and what it does.
{ /** The button's words: "Send me a sign-in link instead". */ label: string; /** Why someone might choose it, in the app's words: "You sign in on your own device; the agent waits." */ why: string; /** Called when the person chooses it. */ onChoose: () => void; }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

AgentIdentity from @/lib/candor/chat
{ id: string; /** The displayed name, e.g. "Architect agent". Never derived from model output. */ name: string; role: AgentRole; specialty?: string | undefined; }
HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6
AgentRole from @/lib/candor/chat
What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).
string