Skip to main content
Every Theo completion returns a follow_ups: Array<{ label, prompt }> field. These are AI-generated, mode-aware suggestions for the next turn — ready to drop into a chat UI as suggestion chips.

When follow-ups are generated

  • On every text-mode completion (auto, fast, think, code, research, roast, genui, vision, stealth text variants).
  • On image / video / document turns — follow-ups tend to be edit/variation prompts (“Try a wider aspect ratio”, “Make the background lighter”).
  • Not on stt (speech-to-text) or tts (text-to-speech) responses — there’s no natural next turn. The server returns between 0 and 3 follow-ups per response. Empty arrays are possible for terse answers, errors, or when Theo has nothing useful to suggest.

Shape

path=null start=null
interface FollowUp {
  /** Short label suitable for a UI chip (max ~60 chars). */
  label: string;
  /** Full prompt to send on click — safe to use as-is in the next request. */
  prompt: string;
}
label is the user-visible text on the chip. prompt is what you submit as the next prompt field when the user clicks it. The two are usually different — label is a short imperative, prompt is a fully-formed instruction that picks up where the last turn left off.

How they’re generated

Theo passes the turn’s content, tools used, and any artifacts into a lightweight follow-up generator that runs after the main completion. The suggestions are AI-generated, not rules-based, so quality varies with the model tier — think mode produces better follow-ups than fast. There is no “follow-up cost” line item; the generator piggybacks on the same LLM call.

Rendering in a chat UI

A minimal React pattern: chip row rendered below the last assistant bubble.
path=null start=null
{assistantMessage.follow_ups?.length ? (
  <div className="flex flex-wrap gap-2 mt-3">
    {assistantMessage.follow_ups.map((f) => (
      <button
        key={f.label}
        onClick={() => sendPrompt(f.prompt)}
        className="px-3 py-1 rounded-full border text-sm hover:bg-zinc-100"
      >
        {f.label}
      </button>
    ))}
  </div>
) : null}
  • Render the chips after the full message is displayed. For streaming responses, follow_ups only appears on the done event, so wait for it before rendering.
  • When the user clicks a chip, send prompt verbatim as the next prompt. Do NOT concatenate it with the user’s own input — the prompts are self-contained.
  • Hide the chip row once the user starts typing a new message, then re-show the next turn’s chips on completion. This matches the pattern most production chat UIs use.

Streaming

For streaming completions, follow-ups appear on the done event:
path=null start=null
for await (const event of stream) {
  if (event.type === "done") {
    setFollowUps(event.data.follow_ups);
  }
}
See Streaming Completions for the full event schema.

Opting out

If follow-ups clutter your UI, simply ignore the field. There is no per-request toggle \u2014 it is cheap enough to always generate, and some callers (voice, analytics) read it even when it isn’t rendered.