> ## Documentation Index
> Fetch the complete documentation index at: https://fal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Plan cards

> Review, edit, approve, and version the agent's plan before and during a multi-step run.

export const PlanCardComp = () => {
  const FAL_COMP_W = 754;
  const FAL_COMP_H = 338;
  const falEases = {
    lin: t => t,
    out: t => 1 - Math.pow(1 - t, 3),
    expo: t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t),
    inOut: t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
    back: t => 1 + 3.1 * Math.pow(t - 1, 3) + 2.1 * Math.pow(t - 1, 2)
  };
  const falEvalNum = (tr, t) => {
    const k = tr.keys;
    const n = k.length;
    if (t <= k[0][0]) return k[0][1];
    if (t >= k[n - 1][0]) return k[n - 1][1];
    for (let i = 0; i < n - 1; i++) {
      const t0 = k[i][0];
      const t1 = k[i + 1][0];
      if (t >= t0 && t <= t1) {
        const u = t1 === t0 ? 1 : (t - t0) / (t1 - t0);
        return k[i][1] + (k[i + 1][1] - k[i][1]) * tr.ease(u);
      }
    }
    return k[n - 1][1];
  };
  const falMakeTimeline = root => {
    const tracks = [];
    const q = sel => !sel ? [] : typeof sel === "string" ? [...root.querySelectorAll(sel)] : [sel];
    const track = (sel, prop, keys, ease = "out", fmt = null) => {
      const made = q(sel).map(el => ({
        el,
        prop,
        keys,
        ease: falEases[ease],
        fmt
      }));
      tracks.push(...made);
      return made;
    };
    const enter = (sel, at, o = {}) => {
      const {y = 10, x = 0, s = 1, b = 0, d = 0.55, e = "expo"} = o;
      track(sel, "--o", [[at, 0], [at + d * 0.6, 1]]);
      if (y) track(sel, "--y", [[at, y], [at + d, 0]], e);
      if (x) track(sel, "--x", [[at, x], [at + d, 0]], e);
      if (s !== 1) track(sel, "--s", [[at, s], [at + d, 1]], e);
      if (b) track(sel, "--b", [[at, b], [at + d * 0.8, 0]], e);
    };
    const exit = (sel, at, d = 0.45) => track(sel, "--o", [[at, 1], [at + d, 0]], "inOut");
    const typewrite = (el, str, at, rate) => {
      const end = at + str.length * rate;
      track(el, "text", [[at, 0], [end, str.length]], "lin", n => str.slice(0, Math.round(n)));
      return end;
    };
    const sample = t => {
      for (const tr of tracks) {
        const v = falEvalNum(tr, t);
        if (tr.prop === "text" && tr.fmt) {
          const s = tr.fmt(v);
          if (tr.el.textContent !== s) tr.el.textContent = s;
        } else if (tr.prop === "opacity") {
          tr.el.style.opacity = String(v);
        } else {
          tr.el.style.setProperty(tr.prop, String(v));
        }
      }
    };
    return {
      track,
      enter,
      exit,
      typewrite,
      sample
    };
  };
  const useFalComp = build => {
    const stageRef = useRef(null);
    const rootRef = useRef(null);
    useEffect(() => {
      const stage = stageRef.current;
      const root = rootRef.current;
      if (!stage || !root || typeof window === "undefined") return;
      const ro = new ResizeObserver(([entry]) => {
        const width = entry && entry.contentRect.width;
        if (width) stage.style.setProperty("--fal-comp-scale", String(width / FAL_COMP_W));
      });
      ro.observe(stage);
      const tl = falMakeTimeline(root);
      const {duration, still, onReady} = build(tl, root);
      const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
      let disposed = false;
      const ready = Promise.all([...[...root.querySelectorAll("img")].map(im => (im.decode ? im.decode() : Promise.resolve()).catch(() => undefined)), document.fonts ? document.fonts.ready : Promise.resolve()]);
      if (reduced) {
        if (onReady) onReady();
        tl.sample(still);
        ready.then(() => {
          if (disposed) return;
          if (onReady) onReady();
          tl.sample(still);
        });
        return () => {
          disposed = true;
          ro.disconnect();
        };
      }
      tl.sample(0);
      let isReady = false;
      let running = false;
      let inView = true;
      let raf = 0;
      let t = 0;
      let clockMs = 0;
      const step = now => {
        const dt = Math.min(0.05, (now - clockMs) / 1000);
        clockMs = now;
        t += dt;
        if (t >= duration) t -= duration;
        tl.sample(t);
        raf = requestAnimationFrame(step);
      };
      const sync = () => {
        const shouldRun = isReady && !disposed && inView && document.visibilityState === "visible";
        if (shouldRun && !running) {
          running = true;
          clockMs = performance.now();
          raf = requestAnimationFrame(step);
        } else if (!shouldRun && running) {
          running = false;
          cancelAnimationFrame(raf);
        }
      };
      const io = new IntersectionObserver(([entry]) => {
        inView = entry ? entry.isIntersecting : true;
        sync();
      });
      io.observe(root);
      document.addEventListener("visibilitychange", sync);
      ready.then(() => {
        if (disposed) return;
        if (onReady) onReady();
        isReady = true;
        sync();
      });
      return () => {
        disposed = true;
        running = false;
        cancelAnimationFrame(raf);
        io.disconnect();
        ro.disconnect();
        document.removeEventListener("visibilitychange", sync);
      };
    }, []);
    return {
      stageRef,
      rootRef
    };
  };
  const FAL_COMP_BASE_CSS = `
@property --x { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --y { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --s { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --o { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --b { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --h { syntax: "<number>"; inherits: false; initial-value: 150; }
.fal-comp-frame { position: relative; width: 100%; aspect-ratio: 754 / 338; border-radius: 12px; overflow: hidden; margin: 1.5rem 0; }
.fal-comp-stage { position: absolute; inset: 0; overflow: hidden; }
.fal-comp-artboard { position: absolute; left: 0; top: 0; width: 754px; height: 338px; transform-origin: 0 0; transform: scale(var(--fal-comp-scale, 1)); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 14px; -webkit-font-smoothing: antialiased; color: #202022; }
.fal-comp-artboard * { box-sizing: border-box; margin: 0; }
.fal-comp-artboard img { max-width: none; }
.fal-comp-a { transform: translate3d(calc(var(--x, 0) * 1px), calc(var(--y, 0) * 1px), 0) scale(var(--s, 1)); opacity: var(--o, 1); filter: blur(calc(var(--b, 0) * 1px)); will-change: transform, opacity, filter; }
`;
  const falFrame = ({comp, label, css, plate, children}) => <div className="fal-comp-frame">
    <style>{FAL_COMP_BASE_CSS + css}</style>
    <div ref={comp.stageRef} role="img" aria-label={label} className="fal-comp-stage" style={{
    background: plate
  }}>
      <div ref={comp.rootRef} aria-hidden="true" className="fal-comp-artboard">
        {children}
      </div>
    </div>
  </div>;
  const falStroke = (d, color = "#494950", w = 1.2, size = 12, cap = "square") => <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" stroke={color} strokeWidth={w} strokeLinecap={cap} strokeLinejoin="round">
    <path d={d} />
  </svg>;
  const DOT_A = [0.78, 0.93, 0.71, 0.45, 0.64, 0.54, 0.5, 0.78, 0.31, 0.75, 0.18, 0.56, 0.49, 0.27, 0.16, 0.1];
  const build = (tl, root) => {
    const job = (tile, at, d = 0.85) => {
      if (!tile) return;
      tl.track(tile, "--wo", [[at - 0.95, 0], [at - 0.8, 1], [at + 0.08, 1], [at + 0.28, 0]], "lin");
      tl.track(tile, "--wave", [[at - 0.95, -55], [at + 0.08, 155]], "inOut");
      tl.track(tile, "--ao", [[at, 0], [at + d * 0.45, 1]]);
      tl.track(tile, "--ab", [[at, 14], [at + d, 0]], "expo");
      tl.track(tile, "--as", [[at, 1.07], [at + d, 1]], "expo");
      const badge = tile.querySelector("[data-badge]");
      tl.track(badge, "--o", [[at + 0.16, 0], [at + 0.3, 1]]);
      tl.track(badge, "--s", [[at + 0.16, 0.4], [at + 0.62, 1]], "back");
    };
    const check = (sel, at) => {
      tl.track(`${sel} [data-num-n]`, "--no", [[at, 1], [at + 0.16, 0]]);
      tl.track(`${sel} [data-num-ck]`, "--co", [[at + 0.08, 0], [at + 0.28, 1]]);
      tl.track(`${sel} [data-num-ck]`, "--s", [[at + 0.08, 0.45], [at + 0.55, 1]], "back");
    };
    tl.enter("[data-chrome]", 0, {
      y: 14,
      s: 0.985,
      b: 5,
      d: 0.7
    });
    tl.enter("[data-status]", 0.28, {
      y: 6,
      d: 0.45
    });
    tl.track("[data-lbl-run]", "--sweep", [[0.3, 110], [1.4, -40], [1.4, 110], [2.5, -40], [2.5, 110], [3.1, -40]], "inOut");
    tl.track("[data-lbl-run]", "--o", [[3.05, 1], [3.26, 0]]);
    tl.track("[data-lbl-run]", "--y", [[3.05, 0], [3.26, -5]]);
    tl.track("[data-lbl-done]", "--o", [[3.18, 0], [3.44, 1]]);
    tl.track("[data-lbl-done]", "--y", [[3.18, 6], [3.52, 0]], "expo");
    tl.track("[data-chev]", "--o", [[3.3, 0], [3.6, 1]]);
    tl.track("[data-chev]", "--x", [[3.3, -4], [3.6, 0]], "expo");
    tl.track("[data-tmr]", "text", [[0.3, 0], [3.05, 2]], "lin", n => n.toFixed(1).replace(/\.0$/, "") + "s");
    [...root.querySelectorAll("[data-dot]")].forEach((dot, i) => {
      const base = DOT_A[i];
      const keys = [];
      for (let k = 0; k <= 14; k++) keys.push([0.25 + k * 0.22, Math.max(0.08, base * (0.42 + 0.58 * (0.5 + 0.5 * Math.sin(i * 0.37 + k * 1.9))))]);
      keys.push([3.1, base * 0.35]);
      tl.track(dot, "opacity", keys, "inOut");
    });
    tl.enter("[data-plan]", 0.62, {
      y: -16,
      s: 0.975,
      b: 7,
      d: 0.78
    });
    tl.enter("[data-phead]", 0.88, {
      y: 8,
      d: 0.45
    });
    tl.enter("[data-pi1]", 1.06, {
      y: 10,
      d: 0.45
    });
    tl.enter("[data-pi2]", 1.2, {
      y: 10,
      d: 0.45
    });
    tl.enter("[data-pi3]", 1.34, {
      y: 10,
      d: 0.45
    });
    const tiles = [...root.querySelectorAll("[data-tile]")];
    tiles.forEach((tile, i) => tl.enter(tile, 1.55 + i * 0.09, {
      y: 8,
      s: 0.96,
      d: 0.5
    }));
    check("[data-pi1]", 3.2);
    job(tiles[0], 3.34);
    check("[data-pi2]", 4.05);
    job(tiles[1], 4.18);
    job(tiles[3], 4.66);
    job(tiles[4], 4.8);
    job(tiles[5], 4.94);
    check("[data-pi3]", 5.4);
    tl.track(tiles[2] ? tiles[2].querySelector("[data-badge]") : null, "--o", [[1.9, 0], [2.1, 1]]);
    tl.track("[data-toolsbar]", "--o", [[4.1, 0], [4.35, 1], [7.3, 1], [7.6, 0]]);
    tl.track("[data-toolsbar]", "--y", [[4.1, -4], [4.4, 0]], "expo");
    const OUT = 7.6 + 7;
    tl.exit("[data-content]", OUT, 0.42);
    const lblRun = root.querySelector("[data-lbl-run]");
    const lblDone = root.querySelector("[data-lbl-done]");
    const widthTracks = tl.track(lblRun ? lblRun.parentElement : null, "--w", [[3.1, 0], [3.62, 0]], "expo");
    const measure = () => {
      if (!lblRun || !lblDone || !widthTracks[0]) return;
      widthTracks[0].keys = [[3.1, lblRun.offsetWidth], [3.62, lblDone.offsetWidth]];
    };
    measure();
    return {
      duration: OUT + 0.48,
      still: OUT - 1,
      onReady: measure
    };
  };
  const comp = useFalComp(build);
  const TOOL = {
    plus: "M6 2.3v7.4M2.3 6h7.4",
    down: "M6 2.2v5.5M3.8 5.5 6 7.8l2.2-2.3M2.6 9.6h6.8",
    trash: "M2.2 3.4h7.6M4.8 3.4V2.2h2.4v1.2M3.3 3.4l.4 6.3h4.6l.4-6.3"
  };
  const Glyph = ({kind}) => kind === "img" ? <svg viewBox="0 0 10 10"><rect x="1" y="1.8" width="8" height="6.4" rx=".6" fill="none" stroke="currentColor" strokeWidth="1.05" /><circle cx="3.3" cy="4" r=".8" /><path d="M1.5 8.2 3.9 5.6l1.5 1.4 1.3-1.2 1.8 2.4z" /></svg> : kind === "vid" ? <svg viewBox="0 0 10 10"><rect x=".9" y="2.3" width="5.5" height="5.4" rx=".9" /><path d="M7.1 4.3 9.2 2.8v4.4L7.1 5.7z" /></svg> : <svg viewBox="0 0 10 10"><path d="M.8 3.8h1.9L5 1.6v6.8L2.7 6.2H.8z" /><path d="M6.6 3.6c.6.7.6 2.1 0 2.8" fill="none" stroke="currentColor" strokeWidth=".85" strokeLinecap="round" /></svg>;
  const Tile = ({width, kind, src, crop, tools}) => <div data-tile className="fal-comp-a pc-tile" style={{
    width
  }}>
      <div className="pc-plate" />
      {src ? <img src={src} alt="" className={"pc-art" + (crop ? " pc-art-crop" : "")} /> : null}
      <div className="pc-wave" />
      {tools ? <div data-toolsbar className="fal-comp-a pc-bar pc-bar-top">
          <span className="pc-cb" />
          <span className="pc-btns">{["plus", "down", "trash"].map(k => <span key={k} className="pc-btn"><svg viewBox="0 0 12 12" fill="none" stroke="#202022" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"><path d={TOOL[k]} /></svg></span>)}</span>
        </div> : null}
      <div className="pc-bar pc-bar-bot"><span data-badge className={"fal-comp-a pc-badge pc-badge-" + kind}><Glyph kind={kind} /></span><span /></div>
    </div>;
  const Item = ({attr, n, text}) => <div {...{
    [attr]: true
  }} className="fal-comp-a pc-pitem">
      <div className="pc-num">
        <span data-num-n className="pc-numlayer pc-num-n">{n}</span>
        <span data-num-ck className="pc-numlayer pc-num-ck"><svg viewBox="0 0 12 12" fill="none" stroke="#088D2B" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M2.6 6.3 4.8 8.5 9.4 3.7" /></svg></span>
      </div>
      <div className="pc-ptext">{text}</div>
    </div>;
  const css = `
.pc-content { position: absolute; inset: 0; }
.pc-chrome { position: absolute; left: 48px; top: 104px; width: 616px; display: flex; flex-direction: column; gap: 8px; padding: 16px; background: #fff; border-radius: 5.5px; box-shadow: 0 0 0 .92px rgba(18,18,22,.1), 0 20px 25px -5px rgba(0,0,0,.1), 0 10px 10px -5px rgba(0,0,0,.04); }
.pc-status { display: flex; align-items: center; gap: 8px; height: 26px; padding-bottom: 6px; }
.pc-dots { position: relative; width: 11.5px; height: 12px; flex: none; }
.pc-dot { position: absolute; width: 2.1px; height: 2.1px; border-radius: 50%; background: #353535; }
.pc-statlabel { position: relative; flex: none; height: 20px; width: calc(var(--w, 52) * 1px); font-size: 16px; font-weight: 500; line-height: 140%; letter-spacing: -.02em; white-space: nowrap; }
.pc-lyr { position: absolute; left: 0; top: 50%; opacity: var(--o, 1); transform: translateY(calc(-50% + var(--y, 0) * 1px)); }
.pc-lyr-done { color: #202022; }
.pc-shimmer { background: linear-gradient(90deg, #a6a6ae 0%, #a6a6ae 38%, #15151c 50%, #a6a6ae 62%, #a6a6ae 100%); background-size: 280% 100%; background-position: calc(var(--sweep, 110) * 1%) 0; -webkit-background-clip: text; background-clip: text; color: transparent; }
.pc-meta { font-size: 14px; font-weight: 500; letter-spacing: -.02em; color: #a6a6ae; }
.pc-meta-timer { color: #787881; font-variant-numeric: tabular-nums; }
.pc-chev { display: grid; place-items: center; width: 16px; height: 16px; }
.pc-chev svg { width: 16px; height: 16px; }
.pc-row { display: flex; align-items: center; gap: 8px; }
.pc-tile { position: relative; height: 150px; flex: none; overflow: hidden; background: rgba(18,18,22,.05); border: 1px solid rgba(18,18,22,.1); border-radius: 4px; }
.pc-row-r2 .pc-tile { height: 160px; }
.pc-plate { position: absolute; inset: 0; background: rgba(18,18,22,.1); }
.pc-art { position: absolute; left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; opacity: var(--ao, 0); transform: scale(var(--as, 1)); filter: blur(calc(var(--ab, 0) * 1px)); }
.pc-art-crop { height: auto; transform-origin: 50% 0; }
.pc-wave { position: absolute; inset: 0; opacity: var(--wo, 0); background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.9) 50%, rgba(255,255,255,0) 100%); background-size: 55% 100%; background-repeat: no-repeat; background-position: calc(var(--wave, -55) * 1%) 0; }
.pc-bar { position: absolute; left: 0; right: 0; height: 28px; padding: 4px; display: flex; align-items: center; justify-content: space-between; }
.pc-bar-top { top: 0; } .pc-bar-bot { bottom: 0; }
.pc-cb { width: 16px; height: 16px; border: 1px solid rgba(255,255,255,.3); background: rgba(255,255,255,.05); backdrop-filter: blur(2px); border-radius: 4px; }
.pc-btns { display: flex; gap: 4px; }
.pc-btn { width: 20px; height: 20px; display: grid; place-items: center; border-radius: 4px; background: rgba(255,255,255,.7); backdrop-filter: blur(6px); box-shadow: 0 3px 3px -1.5px rgba(0,0,0,.06), 0 1px 1px -.5px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.06); }
.pc-btn svg { width: 12px; height: 12px; }
.pc-badge { width: 16px; height: 16px; display: grid; place-items: center; overflow: hidden; border: 1px solid rgba(18,18,22,.1); border-radius: 4px; }
.pc-badge svg { width: 10px; height: 10px; display: block; }
.pc-badge-img { background: #3386fe; } .pc-badge-img svg { fill: #eef6ff; color: #eef6ff; }
.pc-badge-vid { background: #088d2b; } .pc-badge-vid svg { fill: #eefff1; color: #eefff1; }
.pc-badge-aud { background: #ffb320; } .pc-badge-aud svg { fill: #461602; color: #461602; }
.pc-plan { position: absolute; left: 233px; top: 41px; width: 475px; overflow: hidden; background: #fff; border-radius: 5.2px; box-shadow: 0 0 0 1.3px rgba(18,18,22,.1), 0 26px 32px -6.5px rgba(0,0,0,.1), 0 13px 13px -6.5px rgba(0,0,0,.04); }
.pc-phead { display: flex; align-items: center; gap: 10.4px; height: 52px; padding: 7.8px 5.2px 7.8px 10.4px; border-bottom: 1.3px solid rgba(18,18,22,.05); }
.pc-phead svg { width: 17px; height: 17px; flex: none; }
.pc-ptitle { font-size: 15.6px; font-weight: 600; line-height: 120%; letter-spacing: -.02em; color: #202022; }
.pc-pitem { display: flex; align-items: center; gap: 10.4px; height: 41.5px; padding: 5.2px 5.2px 5.2px 10.4px; border-bottom: 1.3px solid rgba(18,18,22,.05); }
.pc-pitem:last-child { border-bottom: 0; }
.pc-num { position: relative; width: 16.4px; height: 18.2px; flex: none; background: rgba(18,18,22,.05); border-radius: 5.2px; }
.pc-numlayer { position: absolute; inset: 0; display: grid; place-items: center; }
.pc-num-n { font-size: 13px; font-weight: 600; letter-spacing: -.01em; color: #19191a; opacity: var(--no, 1); }
.pc-num-ck { opacity: var(--co, 0); transform: scale(var(--s, 1)); }
.pc-num-ck svg { width: 10px; height: 10px; }
.pc-ptext { font-size: 15.6px; font-weight: 500; line-height: 120%; letter-spacing: -.02em; color: #494950; white-space: nowrap; }
`;
  const A = "/images/agent/comps/";
  return falFrame({
    comp,
    css,
    plate: "linear-gradient(135deg, #d9d3c7 0%, #cfc7ba 55%, #c3bbae 100%)",
    label: "A fal Agent plan card whose steps check off as image, video, and 3D generations land in the grid below",
    children: <div data-content className="fal-comp-a pc-content" style={{
      "--o": 0
    }}>
        <div data-chrome className="fal-comp-a pc-chrome">
          <div data-status className="fal-comp-a pc-status">
            <div className="pc-dots">{DOT_A.map((_, i) => <span key={i} data-dot className="pc-dot" style={{
      left: i % 4 * 3.28,
      top: Math.floor(i / 4) * 3.28
    }} />)}</div>
            <div className="pc-statlabel">
              <span data-lbl-run className="pc-lyr pc-shimmer">Thinking</span>
              <span data-lbl-done className="pc-lyr pc-lyr-done" style={{
      "--o": 0
    }}>Running the plan</span>
            </div>
            <span className="pc-meta">·</span>
            <span data-tmr className="pc-meta pc-meta-timer">0s</span>
            <span data-chev className="fal-comp-a pc-chev"><svg viewBox="0 0 16 16" fill="none" stroke="#A6A6AE" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"><path d="M6.4 4.4 10 8l-3.6 3.6" /></svg></span>
          </div>
          <div className="pc-row">
            <Tile width={150} kind="img" src={A + "mm-t0.webp"} tools />
            <Tile width={211} kind="vid" src={A + "mm-t1.webp"} />
            <Tile width={207} kind="aud" />
          </div>
          <div className="pc-row pc-row-r2">
            <Tile width={150} kind="img" src={A + "mm-r2a.webp"} crop />
            <Tile width={272} kind="img" src={A + "mm-r2b.webp"} crop />
            <Tile width={146} kind="img" src={A + "mm-r2c.webp"} crop />
          </div>
        </div>
        <div data-plan className="fal-comp-a pc-plan">
          <div data-phead className="fal-comp-a pc-phead">
            <svg viewBox="0 0 21 21" fill="none" stroke="#787881" strokeWidth="1.56" strokeLinecap="round" strokeLinejoin="round"><path d="M3.2 5.4 4.6 6.8 7.1 4.2M9.6 5.6h8M9.6 10.5h8M9.6 15.4h8M3.4 10.5h1.7M3.4 15.4h1.7" /></svg>
            <div className="pc-ptitle">Spore lights, concept to animation</div>
          </div>
          <Item attr="data-pi1" n={1} text="Generate concept art" />
          <Item attr="data-pi2" n={2} text="Build a 3D turntable from the concept" />
          <Item attr="data-pi3" n={3} text="Animate the walk cycle" />
        </div>
      </div>
  });
};

A plan card is the agent's proposal for multi-step work. It appears in the chat as a titled, numbered list of steps. Each step can carry a model chip, a short reason, and an approval checkpoint. A **Why this plan** disclosure explains the overall approach.

Simple requests do not produce a plan card. The agent runs them directly.

<PlanCardComp />

## Read the card

| Element                     | Meaning                                                           |
| :-------------------------- | :---------------------------------------------------------------- |
| Step label                  | What the step produces                                            |
| Model chip                  | The model the agent intends to use. Empty means Auto              |
| Reason                      | One line on why this step or model                                |
| **Pause here for approval** | The run stops before this step until you approve it               |
| Status badge                | **Running**, **Checking status…**, or **Another plan is running** |

## Edit the plan

You can edit the card directly before you run it, and you can edit the remaining steps while it runs.

* **Rename** a step by clicking its label.
* **Reorder** steps by dragging them.
* **Add** or **remove** steps with the controls on each row. Removing a step also removes the steps that depend on it.
* **Pin a model** on a step by clicking its model chip. A pinned model is final. The agent does not replace it.
* **Toggle an approval checkpoint** on any step.

The agent sees your edits. When it continues, it works from the edited plan, not its original proposal.

## Run, cancel, or revise

* **Run plan** starts execution. Every remaining step becomes a visible row in the [queue](/docs/documentation/agent/chats/queue), so you can see the whole tail.
* **Cancel plan** tells the agent you do not want this plan. The agent treats it as feedback and proposes a revision.
* **Not happy with the plan?** Type a change in the text box on the card. This sends a normal message and produces a new plan version.

Keyboard: `Cmd+Enter` (`Ctrl+Enter`) submits the revision box.

## Approval checkpoints

A checkpoint is a hard stop. The agent cannot run past an unapproved checkpoint, even if it wants to. When execution reaches one, the chat row in the sidebar shows **Needs approval**, and the queue row waits. Approve it on the card to continue. Approval is consumed once, so the same step does not ask twice.

Checkpoints are separate from [spending caps](/docs/documentation/agent/spending-caps), which gate on cost.

## Plan versions

One chat has one logical plan with a version history.

* A re-render in the same turn updates the card in place.
* A re-plan in a later turn creates a new card, **v2**, **v3**, and so on, at the bottom of the chat.
* The old card freezes as superseded and shows **Jump to the latest plan version**.

## Steps that continue automatically

When a step's run lands, the agent continues with the next step without a message from you. The card shows the next step as **Up next**. If a batch lands partially, the agent continues with the runs that succeeded and reports the rest.

You can interrupt at any time. See [Queue and steering](/docs/documentation/agent/chats/queue) for how a message sent mid-run is handled, and how to halt or resume a chain.
