22 KiB
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):
/* 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:
/* 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:
/* Expanded card inside compact grid */
.compactGridExpanded {
grid-column: span 2;
}
- Step 4: Add animation classes
Append to AgentHealth.module.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:
/* 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:
.statStrip {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
margin-bottom: 16px;
}
to:
.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:
/* 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
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:
import { ExternalLink, RefreshCw, Pencil } from 'lucide-react';
to:
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:
const [viewMode, setViewMode] = useState<'compact' | 'expanded'>(() => {
const saved = localStorage.getItem('cameleer:runtime:viewMode');
return saved === 'expanded' ? 'expanded' : 'compact';
});
const [expandedApps, setExpandedApps] = useState<Set<string>>(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 <div className={styles.statStrip}>, after the last <StatCard> (the "Dead" one ending around line 401), add:
<div className={styles.viewToggle}>
<button
className={`${styles.viewToggleBtn} ${viewMode === 'compact' ? styles.viewToggleBtnActive : ''}`}
onClick={() => toggleViewMode('compact')}
title="Compact view"
>
<LayoutGrid size={14} />
</button>
<button
className={`${styles.viewToggleBtn} ${viewMode === 'expanded' ? styles.viewToggleBtnActive : ''}`}
onClick={() => toggleViewMode('expanded')}
title="Expanded view"
>
<List size={14} />
</button>
</div>
- 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
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:
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:
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 (
<div
className={`${styles.compactCard} ${variantClass}`}
onClick={onExpand}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onExpand(); }}
>
<div className={styles.compactCardHeader}>
<span className={styles.compactCardName}>{group.appId}</span>
<ChevronRight size={14} className={styles.compactCardChevron} />
</div>
<div className={styles.compactCardMeta}>
<span className={styles.compactCardLive}>
{group.liveCount}/{group.instances.length} live
</span>
<span className={isHealthy ? styles.compactCardHeartbeat : styles.compactCardHeartbeatWarn}>
{heartbeat ? timeAgo(heartbeat) : '\u2014'}
</span>
</div>
</div>
);
}
- Step 3: Replace the group cards grid with conditional rendering
In AgentHealth.tsx, replace the entire group cards grid block (lines 515-569):
{/* Group cards grid */}
<div className={isFullWidth ? styles.groupGridSingle : styles.groupGrid}>
{groups.map((group) => (
<GroupCard
...
</GroupCard>
))}
</div>
with:
{/* Group cards grid */}
{viewMode === 'expanded' || isFullWidth ? (
<div className={isFullWidth ? styles.groupGridSingle : styles.groupGrid}>
{groups.map((group) => (
<GroupCard
key={group.appId}
title={group.appId}
accent={appHealth(group)}
headerRight={
<Badge
label={`${group.liveCount}/${group.instances.length} LIVE`}
color={appHealth(group)}
variant="filled"
/>
}
meta={
<div className={styles.groupMeta}>
<span><strong>{group.totalTps.toFixed(1)}</strong> msg/s</span>
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
<span>
<StatusDot
variant={
appHealth(group) === 'success'
? 'live'
: appHealth(group) === 'warning'
? 'stale'
: 'dead'
}
/>
</span>
</div>
}
footer={
group.deadCount > 0 ? (
<div className={styles.alertBanner}>
<span className={styles.alertIcon}>⚠</span>
<span>
Single point of failure —{' '}
{group.deadCount === group.instances.length
? 'no redundancy'
: `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
</span>
</div>
) : undefined
}
>
<DataTable<AgentInstance & { id: string }>
columns={instanceColumns as Column<AgentInstance & { id: string }>[]}
data={group.instances.map(i => ({ ...i, id: i.instanceId }))}
onRowClick={handleInstanceClick}
pageSize={50}
flush
/>
</GroupCard>
))}
</div>
) : (
<div className={styles.compactGrid}>
{groups.map((group) =>
expandedApps.has(group.appId) ? (
<div key={group.appId} className={styles.compactGridExpanded}>
<GroupCard
title={group.appId}
accent={appHealth(group)}
headerRight={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Badge
label={`${group.liveCount}/${group.instances.length} LIVE`}
color={appHealth(group)}
variant="filled"
/>
<button
className={styles.collapseBtn}
onClick={() => toggleAppExpanded(group.appId)}
title="Collapse"
>
<ChevronDown size={14} />
</button>
</div>
}
meta={
<div className={styles.groupMeta}>
<span><strong>{group.totalTps.toFixed(1)}</strong> msg/s</span>
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
<span>
<StatusDot
variant={
appHealth(group) === 'success'
? 'live'
: appHealth(group) === 'warning'
? 'stale'
: 'dead'
}
/>
</span>
</div>
}
footer={
group.deadCount > 0 ? (
<div className={styles.alertBanner}>
<span className={styles.alertIcon}>⚠</span>
<span>
Single point of failure —{' '}
{group.deadCount === group.instances.length
? 'no redundancy'
: `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
</span>
</div>
) : undefined
}
>
<DataTable<AgentInstance & { id: string }>
columns={instanceColumns as Column<AgentInstance & { id: string }>[]}
data={group.instances.map(i => ({ ...i, id: i.instanceId }))}
onRowClick={handleInstanceClick}
pageSize={50}
flush
/>
</GroupCard>
</div>
) : (
<CompactAppCard
key={group.appId}
group={group}
onExpand={() => toggleAppExpanded(group.appId)}
/>
),
)}
</div>
)}
- 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
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:
import { useState, useMemo, useCallback } from 'react';
to:
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:
const [animatingApps, setAnimatingApps] = useState<Map<string, 'expanding' | 'collapsing'>>(new Map());
const animationTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(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:
expandedApps.has(group.appId) ? (
<div key={group.appId} className={styles.compactGridExpanded}>
<GroupCard
to:
expandedApps.has(group.appId) ? (
<div
key={group.appId}
className={`${styles.compactGridExpanded} ${styles.expandWrapper} ${
animatingApps.get(group.appId) === 'expanding'
? styles.expandWrapperCollapsed
: animatingApps.get(group.appId) === 'collapsing'
? styles.expandWrapperCollapsed
: styles.expandWrapperExpanded
}`}
>
<GroupCard
Update the collapse button onClick:
onClick={() => animateToggle(group.appId)}
Update the CompactAppCard's onExpand:
<CompactAppCard
key={group.appId}
group={group}
onExpand={() => 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
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
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
git add .claude/rules/ui.md
git commit -m "docs: add compact view to runtime section of ui rules"