Host Integration
Design a stable interaction boundary between UI components and the downcity main package
Host Integration
This page answers a more important question:
How should components in @downcity/ui interact with the downcity main package?
The answer is not “import internal modules from the main package directly”.
The recommended model is a three-layer split:
downcitymain package exposes stable gateway capabilities- the host app wraps those capabilities in adapters or hooks
@downcity/uionly renders state and emits interaction intent
Why this split is better
- no dependency on
packages/downcity/*internal source paths - safer production builds
- UI primitives stay reusable across hosts
- runtime logic, business orchestration, and presentation stay separate
What to avoid
import { someInternalRuntimeMethod } from "../../../packages/downcity/src/console/ui/xxx";Avoid this pattern because it couples UI code to the source layout of the main package.
Recommended package boundary
packages/downcity
└─ exposes /api/ui/* and /api/dashboard/* gateway capabilities
console-ui
└─ wraps request / query / mutation / hook logic
@downcity/ui
└─ provides pure presentation primitives such as Card, Button, Sheet, TabsIf reusable host adapters are needed later, add a separate package such as:
@downcity/ui-bridgeDo not merge that responsibility into @downcity/ui.
The repo already has the right foundation
These files already define a strong runtime boundary:
packages/downcity/src/console/ui/ConsoleUIGateway.tspackages/downcity/src/types/ConsoleUI.tsproducts/console/src/lib/dashboard-api.tsproducts/console/src/hooks/useConsoleDashboard.ts
That means the right direction is already visible:
- the main package exposes stable APIs
- the host app consumes them through adapters
- components receive data and callbacks
Recommended component pattern
Instead of copying the business-heavy map logic from teamprofile, extract the interaction pattern and rebuild it for Downcity.
AgentWorkbenchCard
This is a selectable runtime card that shows status and exposes actions.
It should display
- agent name
- runtime status
- primary model
- channel summary
- last update time
- primary actions
It should not own
- direct data fetching
- API endpoint construction
- main-package imports
- complex domain state orchestration
Recommended data contract
export interface AgentWorkbenchSnapshot {
id: string;
name: string;
projectRoot?: string;
running: boolean;
primaryModelId?: string;
updatedAt?: string;
baseUrl?: string;
statusText?: string;
channels: Array<{
channel: string;
linkState?: string;
statusText?: string;
}>;
}export interface AgentWorkbenchActions {
onSelect: (agentId: string) => void;
onStart?: (agentId: string) => Promise<void> | void;
onStop?: (agentId: string) => Promise<void> | void;
onRestart?: (agentId: string) => Promise<void> | void;
onOpenDetails?: (agentId: string) => void;
}export interface AgentWorkbenchCardProps
extends AgentWorkbenchActions {
snapshot: AgentWorkbenchSnapshot;
selected?: boolean;
busy?: boolean;
}The important part is simple:
- the component receives a
snapshot - the component emits
intent - the host layer performs the real network action
Recommended interaction flow
gateway returns agent list
-> console-ui adapter maps response into snapshot
-> AgentWorkbenchCard renders
-> user clicks restart
-> adapter calls /api/ui/agents/restart
-> hook refreshes agents / overview / services
-> component receives next snapshot and rerendersThis preserves the most valuable lesson from teamprofile:
- the component visualizes the current state
- the container orchestrates interactions
- the domain layer executes commands
Recommended host implementation
In console-ui, keep interaction logic inside queries, mutations, and hooks instead of moving it into UI primitives.
function AgentWorkbenchCardContainer(props: { agentId: string }) {
const dashboard = useConsoleDashboard();
const snapshot = dashboard.agents.find((item) => item.id === props.agentId);
if (!snapshot) return null;
return (
<AgentWorkbenchCard
snapshot={{
id: snapshot.id,
name: snapshot.name,
projectRoot: snapshot.projectRoot,
running: Boolean(snapshot.running),
primaryModelId: snapshot.primaryModelId,
updatedAt: snapshot.updatedAt,
baseUrl: snapshot.baseUrl,
channels:
snapshot.chatProfiles?.map((item) => ({
channel: item.channel || "unknown",
linkState: item.linkState,
statusText: item.statusText,
})) || [],
}}
selected={dashboard.selectedAgentId === snapshot.id}
onSelect={dashboard.selectAgent}
onRestart={dashboard.restartAgent}
onStop={dashboard.stopAgent}
onOpenDetails={() => {
// Host app decides whether to open a Sheet, Dialog, or inspector panel.
}}
/>
);
}Recommended composition
This pattern works well with:
Cardfor the summary shellBadgefor runtime and connection statesButtonfor primary actionsDropdownMenufor secondary actionsSheetfor detailsTabsfor overview, services, logs, and config
That gives Downcity its own “overview + inspector” interaction model without importing business simulation logic from another project.
What to borrow from teamprofile
Borrow:
- the main view plus inspector relationship
- selected-state-driven detail rendering
- local actions attached to the active entity
- presentation driven by structured state
Do not borrow:
- business simulation logic inside the UI package
- pathfinding and animation scheduling inside reusable primitives
- UI owning domain truth
Package ownership recommendation
@downcity/ui
Keep:
- base primitives
- layout primitives
- pure presentational composites
- shared styles and exported public types
console-ui
Keep:
dashboard-apidashboard-queriesdashboard-mutations- page-level hooks
- container components
packages/downcity
Keep:
- gateway routes
- runtime control
- aggregated agent state
- shared runtime types
Best first composite components
If you want to turn this design into actual reusable UI, the best first batch is:
AgentWorkbenchCardAgentChannelStatusListAgentInspectorSheetRuntimeMetricCardCommandActionBar
All of them can be composed from @downcity/ui primitives while talking to the main package through the host adapter layer.
Conclusion
For Downcity, the correct solution is not “move a business component into the UI package”.
It is:
- keep reusable visuals in
@downcity/ui - keep main-package interaction in
console-ui - connect them through a stable
snapshot + intentcontract
If you want to keep going, the best next implementation target is AgentWorkbenchCard + AgentInspectorSheet.