# Agent workspace

> Organizes missions, thinking, tool execution, human approval, and evidence into one continuous agent product page.

A shadcn/ui block from the [@motion-lexicon](https://motion-lexicon.pages.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/motion-lexicon/agent-workspace).

- Page, with a live preview: https://shadcn.exchange/r/motion-lexicon/agent-workspace
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/motion-lexicon/agent-workspace-light-4f104fea8394a8b7.png
- Registry item: https://motion-lexicon.pages.dev/r/agent-workspace.json
- Categories: page-block
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://motion-lexicon.pages.dev/r/agent-workspace.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `motion`
- Registry items: `https://motion-lexicon.pages.dev/r/inline-edit.json`, `https://motion-lexicon.pages.dev/r/metric-ticker.json`, `https://motion-lexicon.pages.dev/r/prompt-composer.json`

## Notes from the registry

https://motion-lexicon.pages.dev/en/blocks/agent-workspace/

## Files

### `src/registry/blocks/agent-workspace.tsx`

```tsx
"use client";

import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { InlineEdit } from "@/components/motion-lexicon/inline-edit";
import { MetricTicker } from "@/components/motion-lexicon/metric-ticker";
import { PromptComposer } from "@/components/motion-lexicon/prompt-composer";

type Locale = "zh" | "en";
type RunState = "ready" | "thinking" | "working" | "approval" | "complete";
export type AgentWorkspaceBlockProps = { locale?: Locale; className?: string };

const ease = [0.23, 1, 0.32, 1] as const;

export function AgentWorkspaceBlock({
  locale = "en",
  className = "",
}: AgentWorkspaceBlockProps) {
  const reduced = useReducedMotion();
  const copy = locale === "zh" ? zh : en;
  const [mission, setMission] = useState(0);
  const [state, setState] = useState<RunState>("ready");
  const timers = useRef<number[]>([]);
  useEffect(() => () => timers.current.forEach(window.clearTimeout), []);
  const run = () => {
    timers.current.forEach(window.clearTimeout);
    timers.current = [];
    setState("thinking");
    const stages: Array<[RunState, number]> = [
      ["working", reduced ? 0 : 850],
      ["approval", reduced ? 0 : 2100],
    ];
    let elapsed = 0;
    stages.forEach(([next, wait]) => {
      elapsed += wait;
      timers.current.push(window.setTimeout(() => setState(next), elapsed));
    });
  };
  const active = copy.missions[mission];

  return (
    <section
      className={`min-h-[720px] w-full overflow-hidden rounded-[18px] border border-[#b8cbd1]/45 bg-[#edf3f2] text-neutral-950 shadow-[0_28px_60px_-42px_rgba(18,46,51,.46)] dark:border-white/10 dark:bg-[#151515] dark:text-neutral-50 ${className}`}
      data-page-block="agent-workspace"
    >
      <header className="flex min-h-14 items-center gap-3 border-b border-[#b8cbd1]/45 bg-[#f9fdfc] px-4 dark:border-white/10 dark:bg-[#1b1b1b]">
        <a
          href="#relay-workspace"
          className="flex min-h-11 items-center gap-2.5 rounded-lg font-semibold tracking-[-.02em] outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
        >
          <span className="grid size-7 place-items-center rounded-lg bg-neutral-950 text-white dark:bg-neutral-50 dark:text-neutral-950">
            <svg
              viewBox="0 0 24 24"
              className="size-3.5 fill-none stroke-current"
              strokeWidth="1.7"
            >
              <path d="M5 17V7l7 4 7-4v10l-7 4-7-4Z" />
              <path d="m5 7 7-4 7 4M12 11v10" />
            </svg>
          </span>
          Relay
        </a>
        <span className="hidden h-4 w-px bg-neutral-200 sm:block dark:bg-white/10" />
        <span className="hidden text-[11px] text-neutral-500 sm:block dark:text-neutral-400">
          {copy.workspace}
        </span>
        <div className="ml-auto flex items-center gap-2">
          <span
            className={`size-1.5 rounded-full ${state === "complete" ? "bg-emerald-600" : state === "approval" ? "bg-amber-500" : state === "ready" ? "bg-neutral-300 dark:bg-neutral-600" : "bg-neutral-950 dark:bg-neutral-50"}`}
          />
          <span className="text-[10px] text-neutral-500 dark:text-neutral-400">
            {copy.state[state]}
          </span>
        </div>
      </header>
      <div
        className="grid min-h-[664px] lg:grid-cols-[188px_minmax(0,1fr)_220px]"
        id="relay-workspace"
      >
        <aside className="hidden border-r border-neutral-200 bg-[#fafafa] p-3 lg:flex lg:flex-col dark:border-white/10 dark:bg-[#181818]">
          <span className="px-3 py-3 text-[10px] font-medium text-neutral-500 dark:text-neutral-400">
            {copy.missionsLabel}
          </span>
          <nav className="grid gap-1">
            {copy.missions.map((item, index) => (
              <button
                key={item.title}
                type="button"
                onClick={() => {
                  setMission(index);
                  setState("ready");
                }}
                className={`min-h-11 rounded-lg px-3 text-left text-[11px] outline-none focus-visible:ring-2 focus-visible:ring-blue-600 ${mission === index ? "bg-neutral-950 font-medium text-white dark:bg-neutral-50 dark:text-neutral-950" : "text-neutral-500 hover:bg-neutral-100 dark:text-neutral-400 dark:hover:bg-white/5"}`}
              >
                {item.short}
              </button>
            ))}
          </nav>
          <div className="mt-auto border-t border-neutral-200 px-3 pt-4 dark:border-white/10">
            <div className="flex -space-x-1">
              {["R", "D", "Q"].map((agent, index) => (
                <span
                  key={agent}
                  className={`grid size-7 place-items-center rounded-full border-2 border-[#fafafa] text-[8px] font-semibold text-white dark:border-[#181818] ${["bg-neutral-950", "bg-neutral-700", "bg-neutral-500"][index]}`}
                >
                  {agent}
                </span>
              ))}
            </div>
            <strong className="mt-3 block text-[10px]">{copy.team}</strong>
            <span className="text-[9px] text-neutral-400">{copy.teamMeta}</span>
          </div>
        </aside>
        <div className="flex min-w-0 flex-col bg-white dark:bg-[#1b1b1b]">
          <div className="border-b border-neutral-200 px-5 py-6 sm:px-7 dark:border-white/10">
            <code className="text-[10px] text-neutral-500 dark:text-neutral-400">
              {active.kicker}
            </code>
            <h1 className="mt-2 max-w-[620px] text-[26px] font-semibold leading-[1.1] tracking-[-.03em]">
              {active.title}
            </h1>
            <p className="mt-3 max-w-[620px] text-[12px] leading-5 text-neutral-500 dark:text-neutral-400">
              {active.description}
            </p>
            <InlineEdit
              value={active.title}
              label={locale === "zh" ? "任务标题" : "Mission title"}
              className="mt-4 max-w-md"
            />
          </div>
          <div className="grid min-h-14 grid-cols-[74px_1fr] items-center border-b border-neutral-200 px-5 text-[11px] sm:px-7 dark:border-white/10">
            <span className="text-neutral-400">{copy.requestLabel}</span>
            <p className="m-0 text-neutral-700 dark:text-neutral-200">{active.request}</p>
          </div>
          <div className="flex flex-1 flex-col px-5 py-5 sm:px-7">
            <AnimatePresence mode="wait">
              <motion.div
                key={`${mission}-${state}`}
                initial={reduced ? false : { opacity: 0, y: 8 }}
                animate={{ opacity: 1, y: 0 }}
                exit={reduced ? undefined : { opacity: 0, y: -6 }}
                transition={{ duration: reduced ? 0 : 0.3, ease }}
                className="min-h-[286px]"
              >
                {state === "ready" ? (
                  <div>
                    <div className="flex items-center justify-between gap-4 border-b border-neutral-200 pb-4 dark:border-white/10">
                      <div>
                        <strong className="block text-[13px]">{copy.readyTitle}</strong>
                        <span className="mt-1 block text-[10px] text-neutral-500 dark:text-neutral-400">{copy.readyBody}</span>
                      </div>
                      <button
                        type="button"
                        onClick={run}
                        className="min-h-11 shrink-0 rounded-lg bg-neutral-950 px-4 text-[10px] font-semibold text-white outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:bg-neutral-50 dark:text-neutral-950"
                      >
                        {copy.start}
                      </button>
                    </div>
                    <ol>
                      {active.steps.map((step, index) => (
                        <li key={step} className="grid min-h-12 grid-cols-[28px_1fr_auto] items-center gap-3 border-b border-neutral-100 text-[11px] last:border-b-0 dark:border-white/8">
                          <code className="text-[9px] text-neutral-400">0{index + 1}</code>
                          <span>{step}</span>
                          <span className="text-[9px] text-neutral-400">{copy.queued}</span>
                        </li>
                      ))}
                    </ol>
                  </div>
                ) : (
                  <div>
                    <div className="flex items-center gap-3">
                      <span className="relative grid size-8 place-items-center rounded-lg border border-neutral-300 dark:border-white/20">
                        <span className="size-1.5 rounded-full bg-neutral-950 dark:bg-neutral-50" />
                        {state !== "complete" ? (
                          <motion.i
                            className="absolute inset-0 rounded-lg border border-neutral-500/50"
                            animate={
                              reduced
                                ? undefined
                                : { scale: [1, 1.3], opacity: [0.5, 0] }
                            }
                            transition={{ duration: 1.4, repeat: Infinity }}
                          />
                        ) : null}
                      </span>
                      <div>
                        <strong className="block text-[12px]">
                          {copy.runTitle[state]}
                        </strong>
                        <span className="text-[9px] text-neutral-400">
                          {copy.runMeta[state]}
                        </span>
                      </div>
                    </div>
                    <ol className="mt-4 border-t border-neutral-200 dark:border-white/10">
                      {active.steps.map((step, index) => {
                        const visible =
                          state === "complete" ||
                          (state === "approval" && index < 3) ||
                          (state === "working" && index < 2) ||
                          (state === "thinking" && index < 1);
                        const current =
                          (state === "thinking" && index === 0) ||
                          (state === "working" && index === 1) ||
                          (state === "approval" && index === 2);
                        return (
                          <li
                            key={step}
                            className="grid min-h-12 grid-cols-[28px_1fr_auto] items-center gap-3 border-b border-neutral-100 dark:border-white/8"
                          >
                            <span
                              className={`grid size-5 place-items-center rounded-md border text-[8px] ${visible && !current ? "border-emerald-600 bg-emerald-600 text-white" : current ? "border-neutral-950 text-neutral-950 dark:border-neutral-50 dark:text-neutral-50" : "border-neutral-200 text-neutral-300 dark:border-white/10"}`}
                            >
                              {visible && !current ? "✓" : index + 1}
                            </span>
                            <span
                              className={`text-[11px] ${visible ? "text-neutral-700 dark:text-neutral-200" : "text-neutral-300 dark:text-neutral-600"}`}
                            >
                              {step}
                            </span>
                            <span className="text-[9px] text-neutral-400">{current ? copy.state[state] : visible ? copy.state.complete : copy.state.ready}</span>
                          </li>
                        );
                      })}
                    </ol>
                    {state === "approval" ? (
                      <div className="mt-4 flex items-center gap-3 border-t border-amber-300 pt-4 dark:border-amber-500/30">
                        <span className="text-amber-600">!</span>
                        <span className="text-[10px] text-amber-800 dark:text-amber-200">
                          {copy.approval}
                        </span>
                        <button
                          type="button"
                          onClick={() => setState("complete")}
                          className="ml-auto min-h-11 rounded-lg bg-neutral-950 px-3 text-[9px] font-semibold text-white dark:bg-neutral-50 dark:text-neutral-950"
                        >
                          {copy.approve}
                        </button>
                      </div>
                    ) : null}
                  </div>
                )}
              </motion.div>
            </AnimatePresence>
          </div>
          <PromptComposer
            className="m-4 mt-0 sm:mx-7"
            placeholder={copy.placeholder}
            sendLabel={copy.start}
            addSourcesLabel={copy.attach}
            sources={[{ id: "brief", label: active.kicker, type: "file", connected: true }]}
            onSubmit={() => run()}
          />
        </div>
        <aside className="hidden border-l border-neutral-200 bg-[#fafafa] p-4 lg:block dark:border-white/10 dark:bg-[#181818]">
          <span className="text-[10px] font-medium text-neutral-500 dark:text-neutral-400">
            {copy.evidence}
          </span>
          <MetricTicker
            label={locale === "zh" ? "完成度" : "Mission completion"}
            value={state === "complete" ? 100 : state === "approval" ? 75 : state === "working" ? 50 : state === "thinking" ? 25 : 0}
            format={(value) => `${value}%`}
            delta={state === "complete" ? 100 : 0}
            period={locale === "zh" ? "当前任务" : "this mission"}
            className="mt-4"
          />
          <div className="mt-4 border-t border-neutral-200 dark:border-white/10">
            {active.evidence.map((item, index) => (
              <div
                key={item}
                className="grid min-h-[76px] grid-cols-[22px_1fr] content-center gap-2 border-b border-neutral-200 py-3 dark:border-white/10"
              >
                <span className="grid size-5 place-items-center rounded-md border border-neutral-300 text-[8px] text-neutral-500 dark:border-white/20">
                  {state === "complete" ? "✓" : index + 1}
                </span>
                <div>
                  <strong className="block text-[10px]">{item}</strong>
                  <span className="mt-1 block text-[8px] text-neutral-400">
                    {copy.evidenceState[index]}
                  </span>
                </div>
              </div>
            ))}
          </div>
        </aside>
      </div>
    </section>
  );
}

const en = {
  workspace: "Agent product workspace",
  requestLabel: "Request",
  queued: "Queued",
  evidenceState: ["Verified", "Available", "Recorded"],
  missionsLabel: "Missions",
  team: "Interface team",
  teamMeta: "3 agents · 1 active",
  readyTitle: "Ready to run this mission",
  readyBody: "The agent will show its evidence as it works.",
  start: "Start mission",
  attach: "Add context",
  placeholder: "Ask Relay to continue…",
  evidence: "Evidence",
  approval: "The preview is ready. Publish it to the shared review URL?",
  approve: "Approve",
  state: {
    ready: "Ready",
    thinking: "Thinking",
    working: "Working",
    approval: "Needs approval",
    complete: "Complete",
  },
  runTitle: {
    ready: "Ready",
    thinking: "Reading the request",
    working: "Building the interface",
    approval: "Waiting for approval",
    complete: "Mission complete",
  },
  runMeta: {
    ready: "",
    thinking: "Comparing product context",
    working: "Editing and checking",
    approval: "One decision required",
    complete: "All evidence recorded",
  },
  missions: [
    {
      short: "Agent launch",
      kicker: "Interface mission",
      title: "Ship the agent UI collection",
      description:
        "Turn visible agent work into a product surface people can understand, trust, and want to reuse.",
      request:
        "Build a cohesive set of beautiful components for thinking, tools, approvals, and handoffs.",
      steps: [
        "Map the agent interaction states",
        "Build the component collection",
        "Publish a browser preview",
        "Record acceptance evidence",
      ],
      evidence: ["11 live components", "Reduced motion", "Registry source"],
    },
    {
      short: "Research brief",
      kicker: "Research mission",
      title: "Turn sources into a decision",
      description:
        "Keep retrieval, synthesis, citations, and the next action connected in one calm workspace.",
      request:
        "Compare the strongest AI interface patterns and recommend a visual direction.",
      steps: [
        "Collect primary references",
        "Group interaction patterns",
        "Write the recommendation",
        "Attach cited evidence",
      ],
      evidence: ["8 primary sources", "3 design paths", "Cited answer"],
    },
    {
      short: "Release review",
      kicker: "Review mission",
      title: "Close the release with proof",
      description:
        "Move checks, screenshots, approvals, and deployment into one visible completion path.",
      request:
        "Review the release, fix actionable defects, and return one production address.",
      steps: [
        "Run quality gates",
        "Inspect desktop and mobile",
        "Approve production deploy",
        "Verify the live artifact",
      ],
      evidence: ["197 unit tests", "34 browser checks", "Live artifact"],
    },
  ],
};
const zh = {
  workspace: "Agent 产品工作区",
  requestLabel: "需求",
  queued: "待执行",
  evidenceState: ["已验证", "可使用", "已记录"],
  missionsLabel: "任务",
  team: "界面协作组",
  teamMeta: "3 个 Agent · 1 个执行中",
  readyTitle: "任务已经可以开始",
  readyBody: "Agent 会在执行过程中持续展示证据。",
  start: "开始任务",
  attach: "添加上下文",
  placeholder: "让 Relay 继续…",
  evidence: "执行证据",
  approval: "预览已经完成，是否发布到共享评审地址？",
  approve: "允许发布",
  state: {
    ready: "待开始",
    thinking: "思考中",
    working: "执行中",
    approval: "等待审批",
    complete: "已完成",
  },
  runTitle: {
    ready: "待开始",
    thinking: "正在读取需求",
    working: "正在构建界面",
    approval: "正在等待审批",
    complete: "任务完成",
  },
  runMeta: {
    ready: "",
    thinking: "比较产品上下文",
    working: "编辑并运行检查",
    approval: "需要一个明确决定",
    complete: "所有证据已经记录",
  },
  missions: [
    {
      short: "Agent 组件发布",
      kicker: "界面任务",
      title: "发布 Agent 组件组",
      description:
        "把 Agent 的执行过程转化为人能理解、信任并愿意复用的产品界面。",
      request: "为思考、工具、审批与交接设计一组统一且好看的组件。",
      steps: [
        "梳理 Agent 交互状态",
        "构建组件集合",
        "发布浏览器预览",
        "记录验收证据",
      ],
      evidence: ["11 个实时组件", "支持减弱动效", "Registry 源码"],
    },
    {
      short: "研究简报",
      kicker: "研究任务",
      title: "把来源转化为决策",
      description:
        "让检索、综合、引用与下一步行动在同一个安静工作区中连续发生。",
      request: "比较优秀的 AI 界面模式，并给出视觉方向建议。",
      steps: ["收集一手参考", "整理交互模式", "形成建议", "附上引用证据"],
      evidence: ["8 个一手来源", "3 个设计方向", "带引用的回答"],
    },
    {
      short: "发布验收",
      kicker: "验收任务",
      title: "用证据完成发布",
      description: "把检查、截图、审批与部署组织成一条清楚的完成路径。",
      request: "验收版本，处理可修复问题，最终只返回一个生产地址。",
      steps: ["运行质量门禁", "检查桌面和手机", "批准生产部署", "验证线上产物"],
      evidence: ["197 个单元测试", "34 个浏览器检查", "线上产物"],
    },
  ],
};
```
