Syed Hasnain

Tool Result

Component

About

An execution disclosure with input and output, copyable results, and error recovery with retry.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Tool Result to your project and fits it to your theme.

Code

How Tool Result is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'motion/react'
import ToolResult, { type ToolResultProps } from '@/components/ui/tool-result'

type Call = Pick<ToolResultProps, 'status' | 'summary'> & { duration?: string }

const phases: { calls: Call[]; ms: number }[] = [
  { ms: 1300, calls: [{ status: 'running', summary: 'Searching 576 registry components' }] },
  {
    ms: 1000,
    calls: [
      { status: 'complete', summary: 'Found 33 matches across 7 groups', duration: '1.3s' },
      { status: 'running', summary: 'Reading src/components/ui' },
    ],
  },
  {
    ms: 1500,
    calls: [
      { status: 'complete', summary: 'Found 33 matches across 7 groups', duration: '1.3s' },
      { status: 'complete', summary: 'Read 214 lines from 3 files', duration: '1.0s' },
      { status: 'running', summary: 'Building preview deployment' },
    ],
  },
  {
    ms: 1600,
    calls: [
      { status: 'complete', summary: 'Found 33 matches across 7 groups', duration: '1.3s' },
      { status: 'complete', summary: 'Read 214 lines from 3 files', duration: '1.0s' },
      { status: 'error', summary: 'Build timed out after 30s', duration: '1.5s' },
    ],
  },
  {
    ms: 1400,
    calls: [
      { status: 'complete', summary: 'Found 33 matches across 7 groups', duration: '1.3s' },
      { status: 'complete', summary: 'Read 214 lines from 3 files', duration: '1.0s' },
      { status: 'running', summary: 'Retrying with a clean cache' },
    ],
  },
  {
    ms: 4000,
    calls: [
      { status: 'complete', summary: 'Found 33 matches across 7 groups', duration: '1.3s' },
      { status: 'complete', summary: 'Read 214 lines from 3 files', duration: '1.0s' },
      { status: 'complete', summary: 'Live at preview.pastaui.dev', duration: '2.9s' },
    ],
  },
]

const names = ['Search component catalog', 'Read source files', 'Deploy preview']

const matches = [
  ['tool-result.tsx', 'ai-agents'],
  ['tool-group.tsx', 'ai-agents'],
  ['tool-approval.tsx', 'ai-agents'],
]

export default function Demo() {
  const [phase, setPhase] = useState(2)
  const [elapsed, setElapsed] = useState(0)
  const [open, setOpen] = useState(true)

  useEffect(() => {
    setElapsed(0)
    if (phase <= 1) setOpen(phase === 1)
    const timer = setTimeout(
      () => setPhase((value) => (value + 1) % phases.length),
      phases[phase].ms,
    )
    const tick = setInterval(() => setElapsed((value) => value + 0.1), 100)
    return () => {
      clearTimeout(timer)
      clearInterval(tick)
    }
  }, [phase])

  return (
    <div className="flex min-h-[22rem] w-full max-w-lg flex-col gap-3">
      <AnimatePresence initial={false}>
        {phases[phase].calls.map((call, index) => (
          <motion.div
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0 }}
            initial={{ opacity: 0, y: 10 }}
            key={names[index]}
            transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
          >
            <ToolResult
              duration={call.status === 'running' ? `${elapsed.toFixed(1)}s` : call.duration}
              copyText={index === 0 ? matches.map(([file]) => file).join('\n') : undefined}
              error={
                call.status === 'error'
                  ? 'vite build exceeded the 30s limit while bundling 1,284 modules.'
                  : undefined
              }
              input={
                index === 0 ? '{ "query": "tool", "group": "ai-agents", "limit": 40 }' : undefined
              }
              name={names[index]}
              onRetry={call.status === 'error' ? () => setPhase(4) : undefined}
              onOpenChange={index === 0 ? setOpen : undefined}
              open={index === 0 ? open : undefined}
              output={
                index === 0 ? (
                  <ul className="space-y-1">
                    {matches.map(([file, group], row) => (
                      <motion.li
                        animate={{ opacity: 1, x: 0 }}
                        className="flex justify-between"
                        initial={{ opacity: 0, x: -6 }}
                        key={file}
                        transition={{ delay: 0.15 + row * 0.08 }}
                      >
                        <span>{file}</span>
                        <span>{group}</span>
                      </motion.li>
                    ))}
                    <li className="pt-1">+ 30 more</li>
                  </ul>
                ) : undefined
              }
              status={call.status}
              summary={call.summary}
            />
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}

Dependencies

More by Syed Hasnain

View all
  • Color Picker

    Color Picker

    92
  • Cost Meter

    Cost Meter

    31
  • Message Scroller

    Message Scroller

    26
  • Quota Banner

    Quota Banner

    21
  • Background Inbox

    Background Inbox

    20
  • Commit

    Commit

    19
  • Image Generation Loader

    Image Generation Loader

    18
  • Attachments

    Attachments

    17

Explore more

Part of Pasta UI — browse the full library on 21st.dev.