You will build a coding agent that:
1. Accepts a natural-language description of a UI component
2. Creates a structured context file for Claude Code
3. Claude Code generates a React component in the workspace
4. Next Hook reads the output, saves it, and opens a preview
## Directory Structure
```
assistants/code-agent/
├── package.yao
├── prompts.yml
├── sandbox.yao
├── skills/
│ └── frontend-design/ ← from Anthropic skills repo
│ └── SKILL.md
└── src/
└── index.ts
```
---
## Step 1: Create `package.yao`
```json
{
"name": "Code Agent",
"connector": "$ENV.DEFAULT_CONNECTOR",
"guard": "bearer-jwt",
"optional": {
"history": true
}
}
```
---
## Step 2: Create `prompts.yml`
Claude Code reads this as the system prompt. Keep it concise — the real instructions live in `context.md` and the Skill.
```yaml
- role: system
content: |
You are a coding agent.
Read $WORKDIR/context.md for your task.
Write all output to $WORKDIR/output/.
When done, write a summary to $WORKDIR/output/summary.md.
```
---
## Step 3: Create `sandbox.yao`
```json
{
"version": "2.0",
"computer": {
"image": "yaoapp/tai-sandbox-claude:latest",
"memory": "2GB",
"cpus": 2
},
"runner": {
"name": "claude",
"options": {
"max_turns": 30,
"permission_mode": "bypassPermissions"
}
},
"lifecycle": "session",
"idle_timeout": "30m"
}
```
The `skills/` directory is copied automatically before the Create Hook runs — no `prepare` step needed for Skills.
---
## Step 4: Add the Frontend Design Skill
```bash
# from your app root
mkdir -p assistants/code-agent/skills
cp -r /path/to/anthropics-skills/skills/frontend-design assistants/code-agent/skills/
```
Claude Code will discover and apply the Skill automatically when asked to build frontend components.
---
## Step 5: Write the Hooks
```typescript
// assistants/code-agent/src/index.ts
import { agent, Process } from "@yao/runtime";
// ─── Create Hook ──────────────────────────────────────────────────────────
export function Create(
ctx: agent.Context,
messages: agent.Message[]
): agent.Create {
const input = messages[messages.length - 1]?.content || "";
// Clean previous output
if (ctx.workspace.Exists("output")) {
ctx.workspace.RemoveAll("output");
}
ctx.workspace.MkdirAll("output");
// Write context for Claude Code
ctx.workspace.WriteFile(
"context.md",
`# Task
${input}
# Requirements
- Write a self-contained React component to \`output/Component.tsx\`
- Write the CSS to \`output/Component.css\` if needed
- Write a brief implementation summary to \`output/summary.md\`
- Do not install packages; use only what is already available
`
);
ctx.memory.context.Set("start_time", Date.now());
ctx.memory.context.Set("input", input);
return { messages };
}
// ─── Next Hook ────────────────────────────────────────────────────────────
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
ctx.Send(`Error: ${payload.error}`);
return { data: { status: "error", message: payload.error } };
}
// Verify output
if (!ctx.workspace.Exists("output/Component.tsx")) {
ctx.Send("Claude Code did not produce a component. Please try again.");
return { data: { status: "no_output" } };
}
// Read generated files
const component = ctx.workspace.ReadFile("output/Component.tsx");
const cssExists = ctx.workspace.Exists("output/Component.css");
const css = cssExists ? ctx.workspace.ReadFile("output/Component.css") : "";
const summary = ctx.workspace.Exists("output/summary.md")
? ctx.workspace.ReadFile("output/summary.md")
: "";
const duration =
Date.now() - (ctx.memory.context.Get("start_time") as number);
const input = ctx.memory.context.Get("input") as string;
// Save to database
const record = Process("models.component.Save", {
name: extractComponentName(component),
input,
code: component,
css,
summary,
created_at: new Date().toISOString(),
}) as number;
// Show summary
if (summary) {
ctx.Send(`**Done in ${duration}ms**\n\n${summary}`);
} else {
ctx.Send(`Component generated in ${duration}ms.`);
}
// Navigate to preview
if (record) {
ctx.Send({
type: "action",
props: {
name: "navigate",
payload: { route: `/agents/code-agent/preview?id=${record}` },
},
});
}
return { data: { status: "success", id: record } };
}
// ─── Helpers ──────────────────────────────────────────────────────────────
function extractComponentName(code: string): string {
const match = code.match(/export\s+(?:default\s+)?function\s+(\w+)/);
return match?.[1] || "Component";
}
```
---
## Step 6: Define the Component Model
```json
// models/component.mod.yao
{
"name": "Component",
"table": { "name": "components" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "name", "type": "string" },
{ "name": "input", "type": "text" },
{ "name": "code", "type": "text" },
{ "name": "css", "type": "text", "nullable": true },
{ "name": "summary", "type": "text", "nullable": true },
{ "name": "created_at", "type": "datetime" }
]
}
```
---
## Try It
1. Start Yao: `yao start`
2. Open the Chat UI and navigate to the `code-agent` assistant
3. Type: `Build a dark-mode toggle button with smooth animation`
4. Watch the execution steps stream in (each Claude Code action appears as an `execute` message)
5. When done, the summary appears and the preview opens automatically
---
## What Happened
```
User message
↓
Runner.Prepare (framework — before Create Hook)
→ copy skills/ → .yao/assistants/code-agent/skills/
↓
Create Hook
→ RemoveAll("output")
→ MkdirAll("output")
→ WriteFile("context.md", ...) ← task for Claude Code
→ memory.context.Set("start_time")
↓
Runner.Stream (Claude Code)
→ reads /workspace/context.md
→ discovers frontend-design SKILL.md
→ writes output/Component.tsx
→ writes output/summary.md
↓
Next Hook
→ ReadFile("output/Component.tsx")
→ Process("models.component.Save", ...)
→ Send(summary)
→ Send({ type: "action", navigate to preview })
```
---
## What's Next
You now have a complete CLI Agent. The same pattern applies to any task: write requirements to a file, let Claude Code work, read the results. Add more Skills, richer prompts, and a custom `sandbox.yao` image to handle any domain.