From 23d24487d16bdde9140cdb820f4f1050a0a1d61b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:32:14 +0200 Subject: [PATCH] docs: add runtime compact view implementation plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-04-16-runtime-compact-view.md | 764 ++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-16-runtime-compact-view.md diff --git a/docs/superpowers/plans/2026-04-16-runtime-compact-view.md b/docs/superpowers/plans/2026-04-16-runtime-compact-view.md new file mode 100644 index 00000000..4df07cb9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-runtime-compact-view.md @@ -0,0 +1,764 @@ +# 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" +```