Downcity

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:

  1. downcity main package exposes stable gateway capabilities
  2. the host app wraps those capabilities in adapters or hooks
  3. @downcity/ui only 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.

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, Tabs

If reusable host adapters are needed later, add a separate package such as:

@downcity/ui-bridge

Do 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.ts
  • packages/downcity/src/types/ConsoleUI.ts
  • products/console/src/lib/dashboard-api.ts
  • products/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

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
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
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 rerenders

This preserves the most valuable lesson from teamprofile:

  • the component visualizes the current state
  • the container orchestrates interactions
  • the domain layer executes commands

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.
      }}
    />
  );
}

This pattern works well with:

  • Card for the summary shell
  • Badge for runtime and connection states
  • Button for primary actions
  • DropdownMenu for secondary actions
  • Sheet for details
  • Tabs for 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-api
  • dashboard-queries
  • dashboard-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:

  1. AgentWorkbenchCard
  2. AgentChannelStatusList
  3. AgentInspectorSheet
  4. RuntimeMetricCard
  5. CommandActionBar

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 + intent contract

If you want to keep going, the best next implementation target is AgentWorkbenchCard + AgentInspectorSheet.