feat: agent status icons + hook-based status tracking - #54
Conversation
Status indicators on sidebar session cards showing real-time agent state derived from Claude Code hook events. Server: - Hooks endpoint bypasses auth (curl from PTY has no cookie) - deriveStatus() maps hook events → agent status state machine - Bulk GET /api/hooks returns all session statuses in one call - Tracks currentTool + toolDetail for "Running Bash: npm test" display Dashboard: - AgentStatusIcon component (adapted from 21st.dev card-status-list): green checkmark (idle), yellow triangle (needs input), spinning dashes (working), gray dash (stopped), dashed circle (unknown) - framer-motion AnimatePresence for smooth status transitions - Status label text on sidebar session rows - shadcn CSS variable layer (ThemeVars) for 21st.dev/shadcn compat - biome config: exclude CSS files (Tailwind v4 @theme unsupported) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| const requireAuth: MiddlewareHandler = async (c, next) => { | ||
| // Hook relay endpoint is unauthenticated (called from PTY sessions via curl) | ||
| if (c.req.path.startsWith("/api/hooks")) return next(); | ||
| const token = extractToken(c); |
There was a problem hiding this comment.
🔴 Critical
Problem: requireAuth exempts all /api/hooks paths — GET and POST alike. The comment two lines below (// POST only accepts events; GET endpoints are behind auth via the wildcard above) is factually wrong: this guard is the wildcard, and it bypasses auth unconditionally for every request matching that prefix.
Why it matters: Unauthenticated callers can hit:
GET /api/hooks→ all tracked session IDs + statuses + unread countsGET /api/hooks/:id/status→ individual agent stateGET /api/hooks/:id/notifications→ notification content
And can inject fake events via POST /api/hooks/:id to spoof status indicators. Since this server accepts AUTH_TOKEN (implying networked deployments), this is a real data-exposure + spoofing risk.
Suggested fix:
const requireAuth: MiddlewareHandler = async (c, next) => {
// Only POST relay calls (curl from PTY, no cookie) are unauthenticated.
if (c.req.method === "POST" && c.req.path.startsWith("/api/hooks/")) return next();
const token = extractToken(c);
if (token && safeEqual(token, AUTH_TOKEN)) return next();
return c.json({ error: "Unauthorized" }, 401);
};| agentStates.set(sessionId, { | ||
| ...current, | ||
| ...statusUpdate, | ||
| lastEvent: event, |
There was a problem hiding this comment.
🟡 Warning
Problem: agentStates is never pruned. When SessionEnd fires, the state is updated to "stopped" but never removed from the map.
Why it matters: Over time (server restarts aside), every session ever created accumulates a permanent entry. The bulk GET /api/hooks response grows monotonically and includes stale "stopped" entries forever — wasting memory and bloating the polling payload.
Suggested fix: Delete on SessionEnd rather than keeping a "stopped" tombstone:
if (statusUpdate.status === "stopped") {
agentStates.delete(sessionId);
} else if (statusUpdate.status) {
const current = agentStates.get(sessionId);
agentStates.set(sessionId, {
...current,
...statusUpdate,
lastEvent: event,
updatedAt: timestamp,
});
}The dashboard already falls back to "unknown" for sessions absent from the map, so this is a clean removal.
nox-0x
left a comment
There was a problem hiding this comment.
Nice feature overall — the animated status icons are well-structured and the bulk polling refactor is a solid improvement. Two issues need addressing before merge: the requireAuth bypass exempts ALL /api/hooks routes (GET + POST), not just POST as the comment claims — unauthenticated callers can read session IDs/statuses and inject fake events on any networked deployment. Also, agentStates is never pruned on SessionEnd, accumulating indefinitely; recommend deleting the entry on stop rather than tombstoning it. See inline comments for concrete fixes.
From /polish review of PRs #54-58: Security: - Auth bypass restricted to POST only on /api/hooks/:id (GET endpoints for status/notifications remain behind auth) Bug fix: - Port default in buildEnv() changed from 3100 to 3000 to match server default — hooks were silently posting to wrong port UX: - Unread indicator: "3 · 2m" (count in red + time) instead of confusing blue dot that looked like UI noise - markNotificationsRead only clears badge on server success (prevents flickering badge when server is down) Cleanup: - Remove "use client" directive (Vite project, not Next.js) - Biome format fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: polish — auth bypass, port mismatch, unread indicator, cleanup From /polish review of PRs #54-58: Security: - Auth bypass restricted to POST only on /api/hooks/:id (GET endpoints for status/notifications remain behind auth) Bug fix: - Port default in buildEnv() changed from 3100 to 3000 to match server default — hooks were silently posting to wrong port UX: - Unread indicator: "3 · 2m" (count in red + time) instead of confusing blue dot that looked like UI noise - markNotificationsRead only clears badge on server success (prevents flickering badge when server is down) Cleanup: - Remove "use client" directive (Vite project, not Next.js) - Biome format fixes Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Real-time agent status indicators on sidebar session cards, derived from Claude Code hook events.
Status icons (adapted from 21st.dev):
Key fix: hooks endpoint now bypasses auth middleware (PTY sessions call via curl without cookies).
Test plan
🤖 Generated with Claude Code