Custom components.
Built-in tabs cover most agents. When they don't, you build your own React components and bundle them with your agent.
When you need this
Most agents work with the built-in tabs: board, files, integrations, job description. Custom components are for when your vertical needs a UI that doesn't exist yet.
Examples: a tax form review panel, a contract comparison view, a patient intake dashboard, a custom configuration wizard.
If you can build it in React, you can put it in a Houston tab.
Project setup
Create a small Vite project next to your manifest. This project
compiles to a single bundle.js that Houston loads
at runtime.
File structure
your-agent/
├── houston.json
├── CLAUDE.md
├── icon.png
├── bundle.js ← built output
├── package.json
├── vite.config.ts
├── tsconfig.json
└── src/
└── index.tsx ← your components
package.json
{
"name": "my-agent",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite build --watch",
"build": "vite build"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@houston-ai/core": "*",
"@houston-ai/chat": "*",
"@houston-ai/board": "*",
"@houston-ai/layout": "*"
},
"devDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@types/react": "^19.0.0",
"typescript": "^5.7.0",
"vite": "^6.0.0"
}
}
Houston packages are peer dependencies. Houston provides them at runtime. Your bundle stays small because React and the component libraries aren't included in it.
vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
build: {
lib: {
entry: "src/index.tsx",
formats: ["es"],
fileName: () => "bundle.js",
},
rollupOptions: {
external: [
"react",
"react-dom",
"react/jsx-runtime",
"@houston-ai/core",
"@houston-ai/chat",
"@houston-ai/board",
"@houston-ai/layout",
],
},
outDir: ".",
emptyOutDir: false,
},
});
The key: everything in external is provided by
Houston. Your bundle only contains your code.
Writing a component
Export a named React component from src/index.tsx.
The export name is what you reference in the manifest.
import type { FC } from "react";
interface CustomTabProps {
agent: {
id: string;
name: string;
folderPath: string;
configId: string;
};
readFile: (name: string) => Promise<string>;
writeFile: (name: string, content: string) => Promise<void>;
listFiles: () => Promise<Array<{
path: string;
name: string;
size: number;
}>>;
sendMessage: (text: string) => void;
}
export const Dashboard: FC<CustomTabProps> = ({
agent,
readFile,
writeFile,
sendMessage,
}) => {
return (
<div style={{ padding: 24 }}>
<h2>{agent.name} Dashboard</h2>
<button onClick={() => sendMessage("Generate this week's report")}>
Generate report
</button>
</div>
);
};
Props reference
Houston injects these props into every custom tab component:
| Prop | Type | What it does |
|---|---|---|
agent |
object | The current agent: id, name, folderPath, configId. |
readFile(name) |
function | Read a file from the agent's working directory. |
writeFile(name, content) |
function | Write a file to the agent's working directory. |
listFiles() |
function | List all files in the agent's working directory. |
sendMessage(text) |
function | Send a message to the AI as if the user typed it in the chat. |
These props are your bridge to the workspace. Read config files the AI wrote. Write data the AI can read. Send messages to trigger AI actions. The symmetric participation from Chapter 2 works here too.
Using Houston packages
You can import any @houston-ai component. They're
available at runtime because Houston loads them as shared modules.
import { Button, Card, Badge } from "@houston-ai/core";
import { EventFeed } from "@houston-ai/events";
import { MemoryBrowser } from "@houston-ai/memory";
This means your custom tab can look and feel native. Same buttons, same cards, same design system. Your users won't know the tab is custom.
Wiring it to the manifest
In your houston.json, add a tab with
customComponent pointing to your export name:
{
"id": "my-agent",
"name": "My Agent",
"description": "An agent with a custom dashboard.",
"tabs": [
{ "id": "board", "label": "Tasks", "builtIn": "board", "badge": "activity" },
{ "id": "dashboard", "label": "Dashboard", "customComponent": "Dashboard" },
{ "id": "files", "label": "Files", "builtIn": "files" }
]
}
The customComponent value must match the named
export from your src/index.tsx. You can have
multiple custom tabs in one agent. Just export multiple
components and reference each one.
Build and ship
npm run build
This produces bundle.js in your project root. Push
it to GitHub alongside your manifest:
your-agent/
├── houston.json
├── CLAUDE.md
├── icon.png
└── bundle.js
When someone imports your agent from GitHub, Houston downloads the bundle and loads your custom components automatically.
During development, use npm run dev to rebuild on
every save. Copy the updated bundle.js to
~/.houston/agents/your-agent-id/ and reload
Houston to see changes.