# 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<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:

```tsx
        <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**

```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 (
    <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):

```tsx
      {/* Group cards grid */}
      <div className={isFullWidth ? styles.groupGridSingle : styles.groupGrid}>
        {groups.map((group) => (
          <GroupCard
            ...
          </GroupCard>
        ))}
      </div>
```

with:

```tsx
      {/* 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}>&#9888;</span>
                    <span>
                      Single point of failure &mdash;{' '}
                      {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}>&#9888;</span>
                        <span>
                          Single point of failure &mdash;{' '}
                          {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**

```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<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:

```tsx
            expandedApps.has(group.appId) ? (
              <div key={group.appId} className={styles.compactGridExpanded}>
                <GroupCard
```

to:

```tsx
            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:

```tsx
                        onClick={() => animateToggle(group.appId)}
```

Update the `CompactAppCard`'s `onExpand`:

```tsx
              <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**

```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"
```
