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:

PropTypeWhat 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.