Elements

Elements · Tool use

Code runner

A snippet with a run button, and the output it produced attached below it.

typescript38ms
const queue = createMessageQueue(driver);
queue.enqueue("also add a changeset");
console.log(queue.size);
output
1→ drains when the run settles
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-code-runner"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A code block that stays inert until the user presses run, then attaches its output underneath. With a runtime the code comes from a tool call the user completes by running it; standalone you hold the run state and feed the output in yourself.

Getting started

A tool the user completes by pressing a button, rather than one that executes the moment the model calls it, is a "human" toolkit entry: it has no execute, so the call stays open until the renderer calls addResult.

Register a human toolkit entry

app/run-code-toolkit.tsx
"use client";

import { useState } from "react";
import { defineToolkit, type ToolCallMessagePartComponent } from "@assistant-ui/react";
import { CodeRunner, type RunState } from "@/components/assistant-ui/elements/code-runner";
import { z } from "zod";

type RunArgs = { language: string; code: string };
type RunResult = { output: string[]; durationMs: number };

const RunCodeUI: ToolCallMessagePartComponent<RunArgs, RunResult> = ({ args, result, addResult }) => {
  const [state, setState] = useState<RunState>("idle");
  const [output, setOutput] = useState<string[]>([]);

  return (
    <CodeRunner
      language={args.language}
      code={args.code}
      state={result ? "ok" : state}
      output={result?.output ?? output}
      durationMs={result?.durationMs}
      onRun={async () => {
        setState("running");
        const startedAt = Date.now();
        const lines = await runInSandbox(args.code);
        setOutput(lines);
        addResult({ output: lines, durationMs: Date.now() - startedAt });
      }}
    />
  );
};

const toolkit = defineToolkit({
  run_code: {
    type: "human",
    description: "Run a code snippet after the user presses run.",
    parameters: z.object({ language: z.string(), code: z.string() }),
    render: RunCodeUI,
  },
});

Complete the call from the button

addResult(result) is the only way this call finishes; there's no execute for the runtime to fall back on. Call it exactly once, from inside onRun, once the sandboxed run actually returns.

app/providers.tsx
const config = AuiConfig({ tools: Tools({ toolkit }) });

Mount config the same way as any other toolkit, with <AuiProvider extends={aui} config={config}>.

Anatomy

<div data-slot="code-runner">
  <div>{/* language, optional duration, run button (spinner while running) */}</div>
  <pre>{/* code, always visible */}</pre>
  {/* output block: only once state !== "idle" */}
</div>

The code block is always visible; the output block only mounts once state leaves "idle". Each output line fades in staggered by 80ms per line, and reads in red when state is "error", neutral otherwise. A blinking caret renders only while state is "running", after the lines received so far. The duration badge next to the run button only shows once a duration is known and the run isn't currently in progress.

Examples

Read the code the model sent

args.language and args.code are exactly what RunCodeUI reads above; nothing about the code itself is inferred, it's the tool call's own parameters.

const RunCodeUI: ToolCallMessagePartComponent<RunArgs, RunResult> = ({ args }) => (
  <CodeRunner language={args.language} code={args.code} state="idle" output={[]} />
);

Stream output before completing the call

addResult only needs to be called once the run is fully done; the output shown while it's still going is ordinary local state, updated as lines arrive:

onRun: async () => {
  setState("running");
  const lines: string[] = [];
  for await (const line of runInSandboxStreaming(args.code)) {
    lines.push(line);
    setOutput([...lines]);
  }
  addResult({ output: lines, durationMs: elapsedMs() });
};

Restyle the runner

Both lanes take className on the root. The output block scrolls horizontally through the codeScroll/codeSurface pair from surfaces.tsx, which keeps a long line reachable instead of clipped, so retargeting those tokens restyles this and every other element that scrolls preformatted output the same way.

API reference

run_code toolkit entry

FieldTypeDescription
type"human"No execute; the call stays open until render calls addResult.
parametersZod schemaDeclares the args shape, { language, code } above.
renderToolCallMessagePartComponent<RunArgs, RunResult>The only source of the result for this call.

Render props used

FieldTypeDescription
argsRunArgsThe language and code the model sent.
resultRunResult | undefinedSet once addResult has been called; undefined while still waiting on the button.
addResult(result)(result: RunResult) => voidCompletes the call. Call it once.