affectUI › Components › Agents

Decision Tree

An agent stuck on a decision lays out its options as a tree – the consequences of each branch, the risky ones marked, its own pick highlighted – and you choose a path or write your own.

Part of affectUI Pro. Install: npx shadcn@latest add https://www.affectui.com/r/decision-tree.json · Import from @/components/ui/decision-tree

When to use

  • An agent hits a fork it shouldn't take alone – it asks how to proceed and shows what each way would change
  • Backend calls without much to look at: data cleanups, refunds, rollouts, escalations – the consequences are the content
  • When the agent has a preference but a person decides – its pick is marked, the other ways stay one click away

When not to use

  • Reviewing finished work – a diff, an email, a refund – one item after another: use Review Center
  • One action that only needs a yes or no, with a countdown – use Approval Card
  • A few quick questions with two answers each – use Decision Inbox
  • An agent asking for access to a tool or scope – use Permission Request

Usage

import { DecisionTree, type Decision } from "@/components/ui/decision-tree";

const decisions: Decision[] = [
  {
    id: "dupes",
    question: "3 customers have two active subscriptions – how should I proceed?",
    context: "All three signed up twice during the checkout outage.",
    agent: "Billing agent", model: "Sonnet 5.5", risk: "high",
    options: [
      { label: "Cancel the newer ones", pick: true, effects: ["Cancels 3 subscriptions · reversible", "Sends 3 emails"] },
      // A flagged part: marked in the leaf, its note shows on hover or with F
      { label: "Refund the newer ones", effects: [["Refunds ", { text: "$147 · not reversible", note: "One charge is two cycles old." }]] },
      { label: "Ask the customers", effects: ["Sends 3 emails · waits for answers"] },
    ],
  },
];

export function Decisions() {
  return (
    <DecisionTree
      decisions={decisions}
      onChoose={async (decision, choice) => {
        if ("other" in choice) return await agent.instruct(decision.id, choice.other); // its answer shows under your branch
        await agent.resume(decision.id, choice.option.label);
      }}
    />
  );
}

Props

NameTypeDefaultDescription
decisionsDecision[]{ id, question, context?, agent, model?, icon?, risk?, options } – everything waiting for a call; decided ones leave, new ids join at the end
optionsDecisionOption[]Per decision, two to four – { label, effects?, pick? }; effects are what the branch leads to, one line each; pick marks the agent's own preference
effectsDecisionText[]Per option – a string, or an array of strings and { text, note, source? } flags; flags are marked and explain themselves on hover, focus or F
risk"high" | "medium" | "low"Per decision – high and medium show as a small tag next to the agent
onChoose(decision, choice) => string | void | Promise<string | void>A branch was picked ({ index, option }) or an instruction written ({ other }) – for an instruction, return the agent's short answer
allowOtherbooleantrueShows the "Other…" branch for your own instruction
refRef<DecisionTreeHandle>hover, choose, setOther, send, next, previous, peek
titlestring"Decisions"Heading of the bar
classNamestringExtra class on the root

Keyboard

  • 1 / 2 / 3 / 4: Picks a branch; the number after the last option opens "Other…"
  • Enter: In "Other…": sends your instruction – Shift+Enter starts a new line
  • ArrowRight / ArrowLeft: Next and previous decision (J and K too)
  • F: Shows the agent's note on the next flagged consequence
  • Esc: Closes "Other…" or a note

Accessibility

  • Every branch is a real button labelled with its option and, where it applies, "the agent's pick"; shortcuts are set with aria-keyshortcuts
  • Flagged consequences are focusable and point to their note with aria-describedby (role tooltip)
  • The "Other…" field is a labelled textarea; it has no focus ring of its own – its outline darkens instead, so focus stays visible
  • Picks, instructions and the agent's answers are announced in a polite live region
  • The lines are decoration and hidden from assistive tech

Responsive

  • Built on a container query: wide, the tree runs left to right – question, branches, consequences
  • Under 640px it runs top to bottom – the branches hang off a rail under the question, each with its consequences below it; nothing scrolls sideways
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Review Center – A focused workspace for what your agents want to do – the queue sorted by risk, one item at a time with the shaky spans marked, Approve, Edit, Reject and Skip on single keys, a stamp as each one leaves and the next one rising in
  • Approval Card – An agent asks before it acts – risk badge, preview, Approve and Deny with shortcuts, a pausing countdown, and a smooth morph into a resolved row
  • Decision Inbox – Open decisions with two quick answers each – answered ones confirm and fold away
  • Agent Tree – Who started whom – an orchestrator and its sub-agents as a live tree, with status, cost and time on every card, costs rolling up each branch, and branches that fold to one line