# Runtime Compact View Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a compact/collapsed view mode to the AgentHealth runtime dashboard where each app is a small health-tinted card, expandable inline to the full agent table. **Architecture:** Add `viewMode` and `expandedApps` state to `AgentHealth`. A `CompactAppCard` inline component renders the collapsed card. The existing `GroupCard`+`DataTable` renders expanded apps. A view toggle in the stat strip switches modes. All CSS uses design system variables with `color-mix()` for health tinting. **Tech Stack:** React, CSS Modules, lucide-react, @cameleer/design-system components, localStorage --- ### Task 1: Add compact card CSS classes **Files:** - Modify: `ui/src/pages/AgentHealth/AgentHealth.module.css` - [ ] **Step 1: Add compact grid class** Append to `AgentHealth.module.css` after the `.groupGridSingle` block (line 94): ```css /* Compact view grid */ .compactGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-bottom: 20px; } ``` - [ ] **Step 2: Add compact card base and health variant classes** Append to `AgentHealth.module.css`: ```css /* Compact app card */ .compactCard { background: color-mix(in srgb, var(--card-accent) 8%, var(--bg-raised)); border: 1px solid color-mix(in srgb, var(--card-accent) 25%, transparent); border-left: 3px solid var(--card-accent); border-radius: var(--radius-md); padding: 10px 12px; cursor: pointer; transition: background 150ms ease; } .compactCard:hover { background: color-mix(in srgb, var(--card-accent) 12%, var(--bg-raised)); } .compactCardSuccess { --card-accent: var(--success); } .compactCardWarning { --card-accent: var(--warning); } .compactCardError { --card-accent: var(--error); } .compactCardHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } .compactCardName { font-size: 13px; font-weight: 700; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .compactCardChevron { color: var(--text-muted); flex-shrink: 0; } .compactCardMeta { display: flex; justify-content: space-between; font-size: 11px; } .compactCardLive { font-weight: 600; color: var(--card-accent); } .compactCardHeartbeat { color: var(--text-muted); } .compactCardHeartbeatWarn { color: var(--card-accent); } ``` - [ ] **Step 3: Add expanded-in-compact-grid span class** Append to `AgentHealth.module.css`: ```css /* Expanded card inside compact grid */ .compactGridExpanded { grid-column: span 2; } ``` - [ ] **Step 4: Add animation classes** Append to `AgentHealth.module.css`: ```css /* Expand/collapse animation wrapper */ .expandWrapper { overflow: hidden; transition: max-height 200ms ease, opacity 150ms ease; } .expandWrapperCollapsed { max-height: 0; opacity: 0; } .expandWrapperExpanded { max-height: 1000px; opacity: 1; } ``` - [ ] **Step 5: Add view toggle classes and update stat strip** Append to `AgentHealth.module.css`: ```css /* View mode toggle */ .viewToggle { display: flex; align-items: center; gap: 4px; } .viewToggleBtn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); background: transparent; color: var(--text-muted); cursor: pointer; transition: background 150ms ease, color 150ms ease; } .viewToggleBtn:hover { color: var(--text-primary); } .viewToggleBtnActive { background: var(--running); border-color: var(--running); color: #fff; } ``` - [ ] **Step 6: Update stat strip grid to add auto column** In `AgentHealth.module.css`, change the `.statStrip` rule from: ```css .statStrip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 16px; } ``` to: ```css .statStrip { display: grid; grid-template-columns: repeat(5, 1fr) auto; gap: 10px; margin-bottom: 16px; } ``` - [ ] **Step 7: Add collapse button class for expanded cards** Append to `AgentHealth.module.css`: ```css /* Collapse button in expanded GroupCard header */ .collapseBtn { background: transparent; border: none; color: var(--text-muted); cursor: pointer; padding: 2px 4px; border-radius: var(--radius-sm); line-height: 1; display: inline-flex; align-items: center; gap: 4px; font-size: 11px; } .collapseBtn:hover { color: var(--text-primary); } ``` - [ ] **Step 8: Commit** ```bash git add ui/src/pages/AgentHealth/AgentHealth.module.css git commit -m "style: add compact view CSS classes for runtime dashboard" ``` --- ### Task 2: Add view mode state and toggle to AgentHealth **Files:** - Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx:1-3` (imports) - Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx:100-120` (state) - Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx:339-402` (stat strip JSX) - [ ] **Step 1: Add lucide-react imports** In `AgentHealth.tsx`, change line 3 from: ```tsx import { ExternalLink, RefreshCw, Pencil } from 'lucide-react'; ``` to: ```tsx import { ExternalLink, RefreshCw, Pencil, LayoutGrid, List, ChevronRight, ChevronDown } from 'lucide-react'; ``` - [ ] **Step 2: Add view mode state** In `AgentHealth.tsx`, after the `const [confirmDismissOpen, setConfirmDismissOpen] = useState(false);` line (line 116), add: ```tsx const [viewMode, setViewMode] = useState<'compact' | 'expanded'>(() => { const saved = localStorage.getItem('cameleer:runtime:viewMode'); return saved === 'expanded' ? 'expanded' : 'compact'; }); const [expandedApps, setExpandedApps] = useState>(new Set()); const toggleViewMode = useCallback((mode: 'compact' | 'expanded') => { setViewMode(mode); setExpandedApps(new Set()); localStorage.setItem('cameleer:runtime:viewMode', mode); }, []); const toggleAppExpanded = useCallback((appId: string) => { setExpandedApps((prev) => { const next = new Set(prev); if (next.has(appId)) next.delete(appId); else next.add(appId); return next; }); }, []); ``` - [ ] **Step 3: Add view toggle to stat strip** In `AgentHealth.tsx`, inside the stat strip `
`, after the last `` (the "Dead" one ending around line 401), add: ```tsx
``` - [ ] **Step 4: Verify the dev server compiles without errors** Run: `cd ui && npx vite build --mode development 2>&1 | tail -5` Expected: build succeeds (the toggle renders but has no visual effect on the grid yet) - [ ] **Step 5: Commit** ```bash git add ui/src/pages/AgentHealth/AgentHealth.tsx git commit -m "feat: add view mode state and toggle to runtime dashboard" ``` --- ### Task 3: Add CompactAppCard component and compact grid rendering **Files:** - Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx` - [ ] **Step 1: Add lastHeartbeat helper to helpers section** In `AgentHealth.tsx`, after the `appHealth()` function (after line 81), add: ```tsx function latestHeartbeat(group: AppGroup): string | undefined { let latest: string | undefined; for (const inst of group.instances) { if (inst.lastHeartbeat && (!latest || inst.lastHeartbeat > latest)) { latest = inst.lastHeartbeat; } } return latest; } ``` - [ ] **Step 2: Add CompactAppCard component** In `AgentHealth.tsx`, after the `LOG_SOURCE_ITEMS` constant (after line 97), add: ```tsx function CompactAppCard({ group, onExpand }: { group: AppGroup; onExpand: () => void }) { const health = appHealth(group); const heartbeat = latestHeartbeat(group); const isHealthy = health === 'success'; const variantClass = health === 'success' ? styles.compactCardSuccess : health === 'warning' ? styles.compactCardWarning : styles.compactCardError; return (
{ if (e.key === 'Enter' || e.key === ' ') onExpand(); }} >
{group.appId}
{group.liveCount}/{group.instances.length} live {heartbeat ? timeAgo(heartbeat) : '\u2014'}
); } ``` - [ ] **Step 3: Replace the group cards grid with conditional rendering** In `AgentHealth.tsx`, replace the entire group cards grid block (lines 515-569): ```tsx {/* Group cards grid */}
{groups.map((group) => ( ))}
``` with: ```tsx {/* Group cards grid */} {viewMode === 'expanded' || isFullWidth ? (
{groups.map((group) => ( } meta={
{group.totalTps.toFixed(1)} msg/s {group.totalActiveRoutes}/{group.totalRoutes} routes
} footer={ group.deadCount > 0 ? (
Single point of failure —{' '} {group.deadCount === group.instances.length ? 'no redundancy' : `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
) : undefined } > columns={instanceColumns as Column[]} data={group.instances.map(i => ({ ...i, id: i.instanceId }))} onRowClick={handleInstanceClick} pageSize={50} flush />
))}
) : (
{groups.map((group) => expandedApps.has(group.appId) ? (
} meta={
{group.totalTps.toFixed(1)} msg/s {group.totalActiveRoutes}/{group.totalRoutes} routes
} footer={ group.deadCount > 0 ? (
Single point of failure —{' '} {group.deadCount === group.instances.length ? 'no redundancy' : `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
) : undefined } > columns={instanceColumns as Column[]} data={group.instances.map(i => ({ ...i, id: i.instanceId }))} onRowClick={handleInstanceClick} pageSize={50} flush />
) : ( toggleAppExpanded(group.appId)} /> ), )}
)} ``` - [ ] **Step 4: Verify the dev server compiles without errors** Run: `cd ui && npx vite build --mode development 2>&1 | tail -5` Expected: build succeeds - [ ] **Step 5: Commit** ```bash git add ui/src/pages/AgentHealth/AgentHealth.tsx git commit -m "feat: add compact app cards with inline expand to runtime dashboard" ``` --- ### Task 4: Add expand/collapse animation for single card toggle **Files:** - Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx` - [ ] **Step 1: Add useRef and useEffect to imports** In `AgentHealth.tsx`, change line 1 from: ```tsx import { useState, useMemo, useCallback } from 'react'; ``` to: ```tsx import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; ``` - [ ] **Step 2: Add animating state to track which apps are transitioning** In `AgentHealth.tsx`, after the `toggleAppExpanded` callback, add: ```tsx const [animatingApps, setAnimatingApps] = useState>(new Map()); const animationTimers = useRef>>(new Map()); const animateToggle = useCallback((appIdToToggle: string) => { // Clear any existing timer for this app const existing = animationTimers.current.get(appIdToToggle); if (existing) clearTimeout(existing); const isCurrentlyExpanded = expandedApps.has(appIdToToggle); if (isCurrentlyExpanded) { // Collapsing: start animation, then remove from expandedApps after transition setAnimatingApps((prev) => new Map(prev).set(appIdToToggle, 'collapsing')); const timer = setTimeout(() => { setExpandedApps((prev) => { const next = new Set(prev); next.delete(appIdToToggle); return next; }); setAnimatingApps((prev) => { const next = new Map(prev); next.delete(appIdToToggle); return next; }); animationTimers.current.delete(appIdToToggle); }, 200); animationTimers.current.set(appIdToToggle, timer); } else { // Expanding: add to expandedApps immediately, animate in setExpandedApps((prev) => new Set(prev).add(appIdToToggle)); setAnimatingApps((prev) => new Map(prev).set(appIdToToggle, 'expanding')); // Use requestAnimationFrame to ensure the collapsed state renders first requestAnimationFrame(() => { requestAnimationFrame(() => { setAnimatingApps((prev) => { const next = new Map(prev); next.delete(appIdToToggle); return next; }); }); }); } }, [expandedApps]); // Cleanup timers on unmount useEffect(() => { return () => { animationTimers.current.forEach((timer) => clearTimeout(timer)); }; }, []); ``` - [ ] **Step 3: Update compact grid to use animateToggle and animation classes** In the compact grid section of the JSX, replace the expanded card wrapper and the `CompactAppCard`'s `onExpand` to use `animateToggle` instead of `toggleAppExpanded`. For the expanded card inside the compact grid, wrap the `GroupCard` in the animation wrapper: Change the expanded card branch from: ```tsx expandedApps.has(group.appId) ? (
animateToggle(group.appId)} ``` Update the `CompactAppCard`'s `onExpand`: ```tsx animateToggle(group.appId)} /> ``` - [ ] **Step 4: Verify build** Run: `cd ui && npx vite build --mode development 2>&1 | tail -5` Expected: build succeeds - [ ] **Step 5: Commit** ```bash git add ui/src/pages/AgentHealth/AgentHealth.tsx git commit -m "feat: add expand/collapse animation for compact card toggle" ``` --- ### Task 5: Visual testing and polish **Files:** - Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx` (if adjustments needed) - Modify: `ui/src/pages/AgentHealth/AgentHealth.module.css` (if adjustments needed) - [ ] **Step 1: Start the dev server** Run: `cd ui && npm run dev` - [ ] **Step 2: Test compact view default** Open the runtime dashboard in a browser. Verify: - All apps render as compact cards (tinted background, left border, health colors) - The view toggle shows the grid icon as active - Cards display app name, live count (x/y), last heartbeat - [ ] **Step 3: Test single card expand** Click a compact card. Verify: - It expands inline with animation (fade in + grow) - The expanded card shows the full agent table (DataTable) - It spans 2 columns in the grid - A collapse button (ChevronDown) appears in the header - [ ] **Step 4: Test single card collapse** Click the collapse button on an expanded card. Verify: - It animates back to compact (fade out + shrink) - Returns to compact card form - [ ] **Step 5: Test expand all** Click the list icon in the view toggle. Verify: - All cards instantly switch to expanded GroupCards (no animation) - The list icon is now active - [ ] **Step 6: Test collapse all** Click the grid icon in the view toggle. Verify: - All cards instantly switch to compact cards (no animation) - Per-app overrides are cleared - [ ] **Step 7: Test localStorage persistence** Verify: - Switch to expanded mode, refresh the page — stays in expanded mode - Switch to compact mode, refresh the page — stays in compact mode - [ ] **Step 8: Test app-scoped view (single app route)** Navigate to a specific app route (e.g., `/runtime/some-app`). Verify: - The view toggle still shows but the app-scoped layout uses `groupGridSingle` (full width) - Compact mode is not used when a specific app is selected (`isFullWidth`) - [ ] **Step 9: Fix any visual issues found** Adjust padding, font sizes, colors, or grid breakpoints as needed. All colors must use CSS variables. - [ ] **Step 10: Commit** ```bash git add ui/src/pages/AgentHealth/AgentHealth.tsx ui/src/pages/AgentHealth/AgentHealth.module.css git commit -m "fix: polish compact view styling after visual testing" ``` --- ### Task 6: Update rules file **Files:** - Modify: `.claude/rules/ui.md` - [ ] **Step 1: Add compact view to Runtime description** In `.claude/rules/ui.md`, change: ``` - **Runtime** — live agent status, logs, commands (`ui/src/pages/RuntimeTab/`) ``` to: ``` - **Runtime** — live agent status, logs, commands (`ui/src/pages/RuntimeTab/`). AgentHealth supports compact view (dense health-tinted cards) and expanded view (full GroupCard+DataTable per app). View mode persisted to localStorage. ``` - [ ] **Step 2: Commit** ```bash git add .claude/rules/ui.md git commit -m "docs: add compact view to runtime section of ui rules" ```