Skip to content

feat: agent status icons + hook-based status tracking - #54

Merged
aterrylu merged 2 commits into
mainfrom
terry/agent-status-ui
Mar 24, 2026
Merged

feat: agent status icons + hook-based status tracking#54
aterrylu merged 2 commits into
mainfrom
terry/agent-status-ui

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Summary

Real-time agent status indicators on sidebar session cards, derived from Claude Code hook events.

Status icons (adapted from 21st.dev):

  • Green checkmark — idle/ready
  • Yellow triangle — needs input (blocked on permission)
  • Spinning dashes — working/running tool
  • Gray dash — stopped
  • Dashed circle — unknown (no events yet)

Key fix: hooks endpoint now bypasses auth middleware (PTY sessions call via curl without cookies).

Test plan

  • Restart sessions (picks up AUTONOMOS_SERVER env var)
  • Interact with a session — status icon changes: unknown → ready → working → idle
  • Permission prompt triggers yellow triangle
  • Status label shows "Running Bash: ..." during tool use
  • Sessions started outside dashboard show dashed "unknown" (no hook events)

🤖 Generated with Claude Code

aterrylu and others added 2 commits March 23, 2026 22:25
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>
@aterrylu
aterrylu merged commit 160ff42 into main Mar 24, 2026
1 check passed
@aterrylu
aterrylu deleted the terry/agent-status-ui branch March 24, 2026 05:28
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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 counts
  • GET /api/hooks/:id/status → individual agent state
  • GET /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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

aterrylu added a commit that referenced this pull request Mar 24, 2026
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>
aterrylu added a commit that referenced this pull request Mar 24, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants