Skip to content

Cloud agent workspace

Three-pane agent workspace with a preview panel.

Usage

Usage
import { AgentWorkspace } from '@/components/blocks/agent-01'


<AgentWorkspace />


// Fill a parent that has a height instead of the viewport
<AgentWorkspace height="100%" />


// Your own tasks, models and agent
<AgentWorkspace
  brand={{ name: 'Harbor', logo: <HarborMark /> }}
  tasks={[
    {
      id: 'fix-login',
      title: 'Fix the login redirect loop',
      repo: 'harbor-web',
      branch: 'main',
      pr: 41,
      adds: 12,
      dels: 4,
      status: 'open',
      files: [{ path: 'src/auth/redirect.ts', diff: '@@ -1,3 +1,3 @@\n-old line\n+new line' }],
      thread: [{ id: 'm1', kind: 'user', text: 'The login page loops forever after sign-in.' }],
    },
  ]}
  models={[{ id: 'fast', label: 'Harbor Fast', description: 'Quick edits' }]}
  defaultPanel="closed"
  defaultTab="preview"
  onSend={(message, { taskId, model }) => track('send', { taskId, model: model.id })}
  generateReply={async function* (message, { task, signal }) {
    const response = await fetch('/api/agent', {
      method: 'POST',
      body: JSON.stringify({ task: task.id, message }),
      signal,
    })
    const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader()
    while (true) {
      const { value, done } = await reader.read()
      if (done) break
      yield value
    }
  }}
/>

Install

pnpm dlx shadcn@latest add @pastaui-pro/agent-01

Source

Props

Cloud agent workspace props
PropTypeDefaultDescription
brandAgentBrand= PastaUI and its markName and logo at the top of the sidebar. The logo also sits in the empty state and the desktop taskbar.
userAgentUser= { name: 'Mira Okafor', plan: 'Team plan' }Signed-in user at the bottom of the sidebar: name and an optional second line such as the plan.
tasksAgentTask[]= Five sample tasksSidebar tasks. Each: id, title, repo, optional branch, pr, adds and dels, status ('merged', 'open', 'draft' or 'idle'), optional files, previewUrl and the thread. Tasks with a PR show its number, counts and status, the rest show the repo.
tasks[].threadAgentThreadItem[]= —Items in order. { kind: 'user', text } is your bubble, { kind: 'agent', text } is a reply where [label](href) becomes a link, and { kind: 'activity', label, icon, commands, files } is a collapsible row. Each command: id, command, status ('running', 'ok' or 'error'), optional duration and output.
tasks[].filesAgentFileChange[]= —Changed files for the Files tab. Each: path, a unified diff body with @@ hunk headers, and an optional status ('added', 'modified' or 'deleted'). Counts and line numbers come from the diff.
models / efforts / permissionsAgentOption[]= Four current models (Claude Opus 5.5, Claude Sonnet 5, GPT-6 Astra, Gemini 3.1 Pro); Low, Medium, High; Read only, Ask, Full accessOptions in the composer menus. Each: id, label, an optional one-line description and an optional icon such as the provider logo.
defaultModelId / defaultEffortId / defaultPermissionIdstring= First model / first effort / last permissionOptions selected on first render.
defaultTaskIdstring= First taskTask open on first render.
defaultPanel'auto' | 'open' | 'closed'= 'auto'Whether the workspace panel starts open. Auto opens it when the block is at least 1200px wide. Below 768px it is always a sheet opened from the header.
defaultTab'files' | 'preview' | 'desktop'= 'files'Workspace tab open on first render.
defaultSidebarOpenboolean= trueWhether the sidebar starts open on wide layouts.
simulateActivityboolean= trueAfter each reply, add a sample activity row whose commands tick through, then a short summary.
onSend(message: string, context: AgentSendContext) => void= —Called when a message is sent, with the task id, model, effort and permission.
generateReply(message: string, context: AgentReplyContext) => string | Promise<string> | AsyncIterable<string>= A canned replyProduces the reply. Text is revealed word by word as it arrives. The context holds the task with its thread, the model, effort, permission and an AbortSignal that fires when Stop is pressed.
heightnumber | string= '100dvh'Height of the shell; the panes scroll inside it. Numbers are pixels and strings take any CSS length: '100%' fills a parent that has a height, such as the area under your own header, and 800 gives a fixed frame.
className / stylestring / CSSProperties= —Classes for the root (use height rather than an h-* class) and inline overrides for the --pp-* theme variables.