{
  "schemaVersion": 1,
  "target": "react",
  "name": "agent-workspace",
  "version": "0.2.0",
  "category": "block",
  "title": "Agent Workspace",
  "description": "A complete agent workspace: streaming conversation, approvals, prompt box, run timeline, active tool call, and artifacts.",
  "tags": [
    "agent",
    "workspace",
    "block",
    "chat",
    "approval",
    "timeline"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [
    {
      "name": "agent-run",
      "version": "0.1.1"
    },
    {
      "name": "approval-card",
      "version": "0.2.0"
    },
    {
      "name": "artifact-preview",
      "version": "0.2.0"
    },
    {
      "name": "prompt-composer",
      "version": "0.2.0"
    },
    {
      "name": "run-timeline",
      "version": "0.2.0"
    },
    {
      "name": "streaming-message",
      "version": "0.2.0"
    },
    {
      "name": "tool-call-card",
      "version": "0.2.0"
    }
  ],
  "docsPath": "/components/agent-workspace",
  "props": [
    {
      "name": "run",
      "type": "AgentRun",
      "description": "The run to render, normalized with agent-run's applyEvents and fromSnapshot."
    },
    {
      "name": "onSubmit / onStop",
      "type": "(prompt) => void | Promise<void> / () => void",
      "description": "Prompt box actions."
    },
    {
      "name": "onApprove / onReject",
      "type": "(approvalId: string) => void | Promise<void>",
      "description": "Approval decisions; send them to your backend for verification."
    },
    {
      "name": "composerStatus",
      "type": "\"idle\" | \"submitting\" | \"running\"",
      "description": "Defaults from the run status."
    },
    {
      "name": "banner",
      "type": "ReactNode",
      "description": "Optional notice above the conversation, e.g. reconnecting."
    }
  ],
  "accessibility": [
    "Conversation and run details are labeled regions; the message list is a polite log.",
    "Every status (run, steps, tool call, approvals, artifacts) is shown as text and a glyph, and announced.",
    "Works with motion disabled: cards and messages appear instantly and state changes stay visible as text and glyphs."
  ],
  "states": [
    "queued",
    "running",
    "waiting-for-approval",
    "succeeded",
    "failed",
    "cancelled"
  ],
  "files": [
    {
      "path": "blocks/agent-workspace.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport type { AgentRun, RunStep } from \"../agent/agent-run\";\nimport { ApprovalCard } from \"../agent/approval-card\";\nimport { ArtifactPreview } from \"../agent/artifact-preview\";\nimport { type ComposerStatus, PromptComposer } from \"../agent/prompt-composer\";\nimport { RunTimeline } from \"../agent/run-timeline\";\nimport { MessageScroller, StreamingMessage } from \"../agent/streaming-message\";\nimport { ToolCallCard, type ToolCallStatus } from \"../agent/tool-call-card\";\n\nexport interface AgentWorkspaceProps {\n  /** The run to show, normalized (see agent-run's applyEvents and fromSnapshot). */\n  run: AgentRun;\n  composerStatus?: ComposerStatus;\n  onSubmit: (prompt: string) => void | Promise<void>;\n  onStop?: () => void;\n  onApprove?: (approvalId: string) => void | Promise<void>;\n  onReject?: (approvalId: string) => void | Promise<void>;\n  /** Optional content above the conversation, e.g. a connection notice. */\n  banner?: ReactNode;\n  className?: string;\n}\n\nconst toolStatus: Record<RunStep[\"status\"], ToolCallStatus> = {\n  queued: \"queued\",\n  running: \"running\",\n  succeeded: \"success\",\n  failed: \"error\",\n  skipped: \"queued\",\n};\n\n/**\n * A complete agent workspace: conversation with streaming replies and approvals,\n * a prompt box, the run timeline, the active tool call, and generated artifacts.\n * It renders whatever run you pass and reports user actions through callbacks.\n */\nexport function AgentWorkspace({ run, composerStatus, onSubmit, onStop, onApprove, onReject, banner, className }: AgentWorkspaceProps) {\n  const active = [...run.steps].reverse().find((step) => step.kind === \"tool\" && step.status !== \"skipped\");\n  const contentKey = run.messages.reduce((total, message) => total + message.content.length, 0) + run.approvals.length;\n  const status: ComposerStatus =\n    composerStatus ?? (run.status === \"running\" || run.status === \"waiting-for-approval\" ? \"running\" : \"idle\");\n\n  return (\n    <div\n      className={[\n        \"grid grid-cols-[minmax(0,1fr)] items-start gap-x-6 gap-y-5 text-zinc-950 lg:grid-cols-[minmax(0,1fr)_minmax(0,22rem)] dark:text-white\",\n        className,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      <section aria-label=\"Conversation\" className=\"flex min-h-[28rem] min-w-0 flex-col gap-3\">\n        {banner}\n        <MessageScroller contentKey={contentKey} className=\"max-h-[32rem] min-h-64 flex-1 space-y-5 px-1 pt-1 pb-3\">\n          {run.messages.length === 0 ? (\n            <div className=\"grid h-full min-h-60 place-items-center px-6 text-center\">\n              <div className=\"max-w-xs\">\n                <p className=\"text-sm font-medium text-zinc-950 dark:text-white\">Start by describing a task</p>\n                <p className=\"mt-1 text-sm text-zinc-500\">\n                  The agent's replies stream in here. Its steps, tool calls, and files appear alongside.\n                </p>\n              </div>\n            </div>\n          ) : null}\n          {run.messages.map((message) => (\n            <StreamingMessage key={message.id} role={message.role} content={message.content} status={message.status} error={message.error} />\n          ))}\n          {run.approvals.map((approval) => (\n            <Enter key={approval.id}>\n              <ApprovalCard\n                title={approval.title}\n                description={approval.description}\n                detail={approval.detail}\n                status={approval.status}\n                onApprove={onApprove ? () => onApprove(approval.id) : undefined}\n                onReject={onReject ? () => onReject(approval.id) : undefined}\n              />\n            </Enter>\n          ))}\n        </MessageScroller>\n        <PromptComposer onSubmit={onSubmit} onStop={onStop} status={status} />\n      </section>\n\n      <aside aria-label=\"Run details\" className=\"grid min-w-0 content-start gap-3\">\n        <RunTimeline run={run} />\n        {active ? (\n          <Enter key={active.id}>\n            <ToolCallCard\n              name={active.name}\n              status={toolStatus[active.status]}\n              summary={active.summary}\n              input={active.input}\n              output={active.output}\n              error={active.error}\n              durationMs={active.startedAt && active.finishedAt ? new Date(active.finishedAt).getTime() - new Date(active.startedAt).getTime() : undefined}\n            />\n          </Enter>\n        ) : null}\n        {run.artifacts.map((artifact) => (\n          <Enter key={artifact.id}>\n            <ArtifactPreview\n              name={artifact.name}\n              type={artifact.type}\n              status={artifact.status}\n              path={artifact.path}\n              content={artifact.content}\n              sizeBytes={artifact.sizeBytes}\n              error={artifact.error}\n            />\n          </Enter>\n        ))}\n      </aside>\n    </div>\n  );\n}\n\n/** Plays a short rise-in when a card first appears; instant with reduced motion. */\nfunction Enter({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"min-w-0 transition-[opacity,translate] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none\">\n      {children}\n    </div>\n  );\n}\n"
    }
  ],
  "example": {
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { AgentRun } from \"@/components/seamless/agent/agent-run\";\nimport { AgentWorkspace } from \"@/components/seamless/blocks/agent-workspace\";\n\nconst initialRun: AgentRun = {\n  id: \"run_1\",\n  title: \"Update the rate-limit docs\",\n  status: \"waiting-for-approval\",\n  messages: [{ id: \"m1\", role: \"user\", status: \"complete\", content: \"Update the rate limits and open a pull request.\" }],\n  steps: [\n    { id: \"s1\", name: \"search_docs\", kind: \"tool\", status: \"succeeded\", output: \"Found 3 pages.\", startedAt: \"2026-11-14T10:00:00.600Z\", finishedAt: \"2026-11-14T10:00:01.400Z\" },\n    { id: \"s2\", name: \"edit_files\", kind: \"tool\", status: \"succeeded\", output: \"Changed 1 file.\", startedAt: \"2026-11-14T10:00:01.600Z\", finishedAt: \"2026-11-14T10:00:02.600Z\" },\n  ],\n  approvals: [{ id: \"a1\", title: \"Open a pull request?\", status: \"pending\", detail: \"gh pr create --title \\\"Update rate limits\\\"\" }],\n  artifacts: [{ id: \"f1\", name: \"rate-limits.md\", type: \"text/markdown\", status: \"ready\", content: \"Free: 60/min\\nPro: 600/min\" }],\n};\n\nexport default function AgentWorkspaceExample() {\n  // In your app, build the run from your backend's events with applyEvents (see agent-run).\n  const [run, setRun] = useState(initialRun);\n  const decide = (status: \"approved\" | \"rejected\") => async (id: string) =>\n    setRun((current) => ({ ...current, status: status === \"approved\" ? \"running\" : \"cancelled\", approvals: current.approvals.map((a) => (a.id === id ? { ...a, status } : a)) }));\n\n  return <AgentWorkspace className=\"w-full\" run={run} onSubmit={() => {}} onApprove={decide(\"approved\")} onReject={decide(\"rejected\")} />;\n}\n"
  }
}
