Component Architecture · April 2026

Strong typing, @owner/ui, and why the library grows by evidence.

A response to John Harlow's 4/16 thread. Design rationale and component API documentation for the Owner Agent component layer.

Scope as a component

The Owner Agent's component library isn't decorative. It's a direct response to prompt families surfaced by Daniel's Eval Dash. Every tool call we design corresponds to a scoped, measured family — which means the system grows by evidence, not opinion.

When a new prompt family emerges in the dashboard with meaningful time-recovery potential, we design the component to handle it. When an existing family can be consolidated, we collapse components. The library is a living map of what the agent must do well — grounded in what customers actually ask.

Eval DashsurfacesPrompt familydefinesComponent(s)
Eval Dash surfaces prompt families · families define components

Tool calls ARE components

The agent doesn't invent UI for actions that already have excellent components. It uses yours. A menu-price edit doesn't get a custom diff card — it uses the Hestia menu-item editor, wrapped in an approve/skip contract. A pause-orders decision doesn't get a custom toggle — it uses the Hestia pause-orders component, wrapped the same way.

update_menu_price
$14.00$16.00
Avocado Toast · Breakfast
Confirm
OK

✕ Custom agent diff card

update_menu_price
Avocado Toast
$14.00$16.00
Update price
Skip

✓ Hestia component + agent chrome

Components we'd add to @owner/ui

These components don't exist in @owner/ui yet. We propose contributing them — not as an agent-only parallel library, but as first-class Owner UI primitives that happen to be agent-shaped.

<ToolCallWrapper />Wraps any Owner UI component in approve/skip/undo chrome.
@owner/ui/agent/ToolCallWrapper
interface ToolCallWrapperProps {
  state: 'proposed' | 'approving' | 'completed' | 'rejected'
  approveLabel: string
  onApprove: () => void
  onReject?: () => void
  onUndo?: () => void
  children: React.ReactNode
}
<AgentPanel />Docked/draggable conversation surface.
@owner/ui/agent/AgentPanel
interface AgentPanelProps {
  taskLabel: string
  taskBreadcrumb?: string
  state: 'idle' | 'thinking' | 'working'
  defaultDock?: 'left' | 'right' | 'free'
  onClose?: () => void
  children: React.ReactNode
}
<Composer />Input with attachment, voice (future), and send.
@owner/ui/agent/Composer
interface ComposerProps {
  placeholder?: string
  isThinking?: boolean
  suggestions?: string[]
  onSend: (text: string, attachments?: File[]) => void
  onSuggestion?: (text: string) => void
}
<Mark />The 5-state volumetric agent presence.
@owner/ui/agent/Mark
interface MarkProps {
  state: 'resting' | 'listening' | 'thinking' | 'working' | 'completed'
  size?: number   // default 24px; solid silhouette below 16px
  variant?: 'green' | 'cream' | 'mono'
  showPin?: boolean
}
<CanvasOverlay />Point-and-click edit affordance on host surfaces.
@owner/ui/agent/CanvasOverlay
interface CanvasOverlayProps {
  hoverDelay?: number  // ms before showing (default 200)
  chipLabel?: string   // e.g. "Edit with Owner"
  onChipClick?: () => void
  children: React.ReactNode
}
<MemoryPill />Contextual memory surfacing primitive.
@owner/ui/agent/MemoryPill
interface MemoryPillProps {
  items: string[]
  label?: string
  collapsible?: boolean
}

Strong typing contract

John Harlow's case for a typed UIMessage interface is correct. Strongly-typed agent messages plus a well-modelled component architecture plus @owner/ui isn't three separate projects — it's one system with three sides. The typing forces the architecture; the architecture forces the library use; the library use forces the quality.

type MessageKind = 'text' | 'tool-call' | 'tool-result' | 'system'

interface BaseMessage {
  id: string
  role: 'user' | 'assistant' | 'system'
  timestamp: number
}

interface TextMessage extends BaseMessage {
  kind: 'text'
  content: string
}

interface ToolCallMessage extends BaseMessage {
  kind: 'tool-call'
  toolName: string
  toolUseId: string
  input: Record<string, unknown>
  state: 'proposed' | 'approving' | 'completed' | 'rejected'
  approveLabel?: string
}

interface ToolResultMessage extends BaseMessage {
  kind: 'tool-result'
  toolUseId: string
  content: string
  isError?: boolean
}

type UIMessage = TextMessage | ToolCallMessage | ToolResultMessage

How this plugs into @owner/ui

The mock components in this prototype mirror real Hestia component APIs. A production integration replaces components/hestia/* imports with @owner/ui imports — no markup or prop changes required. The work to migrate is rename-imports, not redesign-everything.

// Before (prototype)
import { MenuItemEditor } from '@/components/hestia/MenuItemEditor'

// After (production)
import { MenuItemEditor } from '@owner/ui'

v0.1 · April 20, 2026 · Derek Orr · Credits: Daniel Ternyak, John Harlow, Jonathan Minori, Chaz Moore, Jack Hanford

← Scenarios