Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Agent Quest is a browser-based monitoring dashboard that visualizes active Claude Code and Codex agent sessions as fantasy heroes in a 2D WoW-style village. Each agent is represented as a hero character that walks between buildings corresponding to its current activity (Read → Library, Edit → Forge, Bash → Arena, etc.).
Agent Quest is a browser-based monitoring dashboard that visualizes active Claude Code, Codex and Cursor agent sessions as fantasy heroes in a 2D WoW-style village. Each agent is represented as a hero character that walks between buildings corresponding to its current activity (Read → Library, Edit → Forge, Bash → Arena, etc.).

## Architecture

Two-process monorepo:

- **server/** — Bun + Hono backend. Two providers run in parallel: `ClaudeProvider` auto-discovers every `~/.claude*` directory with a `projects/` subdir (e.g. `~/.claude`, `~/.claude-work`, `~/.claude-personale`); `CodexProvider` watches `~/.codex/sessions/` for Codex rollout files. Both poll their session logs every 2-3s, parse events into `AgentState` objects, push updates over native Bun WebSocket. Each `AgentState` carries its `configDir` and a `source` field (`'claude' | 'codex'`) so the UI can distinguish installations and providers. Optional Hono endpoint receives Claude Code `postToolUse` hooks for lower-latency events — **Claude Code only**; Codex doesn't expose hooks. `SessionRegistry` (pidfile oracle) is also **Claude-only by design**; Codex liveness is inferred purely from rollout-file activity.
- **server/** — Bun + Hono backend. Three providers run in parallel: `ClaudeProvider` auto-discovers every `~/.claude*` directory with a `projects/` subdir (e.g. `~/.claude`, `~/.claude-work`, `~/.claude-personale`); `CodexProvider` watches `~/.codex/sessions/` for Codex rollout files; `CursorProvider` watches `~/.cursor/projects/**/agent-transcripts/**/*.jsonl` for Cursor IDE agent-chat transcripts. All poll their session logs every 2-3s, parse events into `AgentState` objects, push updates over native Bun WebSocket. Each `AgentState` carries its `configDir` and a `source` field (`'claude' | 'codex' | 'cursor'`) so the UI can distinguish installations and providers. Optional Hono endpoint receives Claude Code `postToolUse` hooks for lower-latency events — **Claude Code only**; Codex and Cursor don't expose hooks. `SessionRegistry` (pidfile oracle) is also **Claude-only by design**; Codex and Cursor liveness is inferred purely from log-file activity.
- **client/** — React 19 + Phaser 4 "Caladan" frontend. Fullscreen Phaser canvas renders the village; React overlay panels (Party Bar, Activity Feed, Detail Panel, Minimap, Top Bar) sit on top via ref-based bridge pattern (useRef + useEffect + EventEmitter).

Data flow: `~/.claude*/projects/**/*.jsonl` and `~/.codex/sessions/**/rollout-*.jsonl` → ClaudeProvider / CodexProvider → SessionParser (per-format) → AgentStateManager → WebSocket → Browser (React state + Phaser scene).
Data flow: `~/.claude*/projects/**/*.jsonl`, `~/.codex/sessions/**/rollout-*.jsonl` and `~/.cursor/projects/<encoded-path>/agent-transcripts/**/*.jsonl` → ClaudeProvider / CodexProvider / CursorProvider → SessionParser (per-format) → AgentStateManager → WebSocket → Browser (React state + Phaser scene).

## Commands

Expand Down Expand Up @@ -45,7 +45,7 @@ These are fixed. Do NOT use 3000, 3333, 5173, 5174, 8000 — reserved by other p

## Key Type: AgentState

The central data model flows from server to client. Defined in shared types. Maps tool calls to activities: Read/Grep/Glob → `reading`, Edit/Write → `editing`, Bash → `bash`, thinking → `thinking`, git → `git`, idle → `idle`, debug → `debugging`, review → `reviewing`. `configDir` can be `~/.claude*` or `~/.codex`; the `source` field (`'claude' | 'codex'`) discriminates which provider produced the session.
The central data model flows from server to client. Defined in shared types. Maps tool calls to activities: Read/Grep/Glob → `reading`, Edit/Write → `editing`, Bash → `bash`, thinking → `thinking`, git → `git`, idle → `idle`, debug → `debugging`, review → `reviewing`. `configDir` can be `~/.claude*`, `~/.codex` or `~/.cursor`; the `source` field (`'claude' | 'codex' | 'cursor'`) discriminates which provider produced the session.

## Design Spec

Expand Down
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</p>

<p align="center">
<strong>A fantasy village dashboard for monitoring your Claude Code CLI and Codex agents.</strong>
<strong>A fantasy village dashboard for monitoring your Claude Code, Codex and Cursor agents.</strong>
</p>

<p align="center">
Expand All @@ -14,9 +14,9 @@

---

> **Use Claude Code CLI or Codex as usual — each agent session auto-spawns a hero on the dashboard, live.**
> **Use Claude Code, Codex or Cursor as usual — each agent session auto-spawns a hero on the dashboard, live.**

Agent Quest is a browser-based monitoring dashboard that visualizes active Claude Code and Codex agent sessions as fantasy heroes in a 2D village. Each running agent becomes a hero who walks between buildings based on what it's doing: `Read` sends it to the Library, `Edit` to the Forge, `Bash` to the Arena, and so on.
Agent Quest is a browser-based monitoring dashboard that visualizes active Claude Code, Codex and Cursor agent sessions as fantasy heroes in a 2D village. Each running agent becomes a hero who walks between buildings based on what it's doing: `Read` sends it to the Library, `Edit` to the Forge, `Bash` to the Arena, and so on.

<p align="center">
<img src="docs/media/day.gif" alt="Agent Quest — main view" width="820" />
Expand All @@ -41,21 +41,21 @@ Agent Quest is a browser-based monitoring dashboard that visualizes active Claud

## Why?

Claude Code and Codex sessions happen in a terminal — useful, but not very *alive*. When you run several agents at once (across projects, across `~/.claude*` installations and `~/.codex`), it's hard to feel what they're actually doing. Agent Quest turns that invisible activity into something you can glance at: a little village where every hero is an agent, and where they walk tells you what they're up to.
Claude Code, Codex and Cursor sessions happen in a terminal — useful, but not very *alive*. When you run several agents at once (across projects, across `~/.claude*` installations, `~/.codex` and `~/.cursor`), it's hard to feel what they're actually doing. Agent Quest turns that invisible activity into something you can glance at: a little village where every hero is an agent, and where they walk tells you what they're up to.

## Features

- Real-time visualization of active Claude Code and Codex sessions
- Auto-discovery of every `~/.claude*` directory (supports multiple installations like `~/.claude-work`, `~/.claude-personale`) and of `~/.codex` if present
- Real-time visualization of active Claude Code, Codex and Cursor sessions
- Auto-discovery of every `~/.claude*` directory (supports multiple installations like `~/.claude-work`, `~/.claude-personale`), of `~/.codex` and of `~/.cursor` if present
- Activity feed, party bar, and detail panel alongside the village scene
- Built-in map editor for customizing the village layout
- Sub-2s latency via native WebSocket (optional lower-latency path via Claude Code `postToolUse` hooks — Claude Code only; Codex doesn't expose hooks)
- Sub-2s latency via native WebSocket (optional lower-latency path via Claude Code `postToolUse` hooks — Claude Code only; Codex and Cursor don't expose hooks)

## Requirements

**Required**
- [Bun](https://bun.sh) 1.1 or later — the runtime behind both the server and the scripts. If you don't have it: `curl -fsSL https://bun.sh/install | bash`
- An active Claude Code or Codex installation (one or more `~/.claude*` directories, and/or `~/.codex`, with session logs). Without either, the dashboard still starts, but the village stays empty and a banner tells you so.
- An active Claude Code, Codex or Cursor installation (one or more `~/.claude*` directories, and/or `~/.codex`, and/or `~/.cursor`, with session logs). Without any of them, the dashboard still starts, but the village stays empty and a banner tells you so.
- See the [Platform matrix](#platform-matrix) below for OS support per provider.

**Optional**
Expand Down Expand Up @@ -144,7 +144,7 @@ lsof -ti:4444,4445 | xargs kill -9
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
```

**Empty village with a "No Claude Code or Codex installation detected" banner** — expected when no `~/.claude*` or `~/.codex` directory with session logs exists. Start a Claude Code or Codex session and heroes appear automatically (the banner disappears on its own).
**Empty village with a "No Claude Code, Codex or Cursor installation detected" banner** — expected when no `~/.claude*`, `~/.codex` or `~/.cursor` directory with session logs exists. Start a Claude Code, Codex or Cursor session and heroes appear automatically (the banner disappears on its own).

**Assets look broken or the app blocks at boot with "missing asset" screens** — see [Missing assets](#missing-assets).

Expand Down Expand Up @@ -224,8 +224,9 @@ Open the UI URL on the other device. The client auto-detects the host so API and
|-------------|-------|----------------------|-------|
| Claude Code | ✓ | ✓ (WSL2 recommended) | ✓ |
| Codex | ✓ | not yet verified | not yet verified |
| Cursor | ✓ | not yet verified | not yet verified |

Claude Code is exercised on macOS and Windows (via WSL2). Codex has been tested on macOS only so far — it should work on Windows/Linux the same way (the provider watches `~/.codex/sessions/`), but we haven't confirmed it yet.
Claude Code is exercised on macOS and Windows (via WSL2). Codex has been tested on macOS only so far — it should work on Windows/Linux the same way (the provider watches `~/.codex/sessions/`), but we haven't confirmed it yet. Cursor support reads the IDE's local agent-chat transcripts (`~/.cursor/projects/`) and has been tested on macOS only so far.

## Windows

Expand Down
4 changes: 2 additions & 2 deletions bin/agentquest
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# agentquest — local dashboard for Claude Code and Codex agents
# agentquest — local dashboard for Claude Code, Codex and Cursor agents
# macOS-first; relies on bash 3.2+, git, bun >= 1.1.
set -euo pipefail

Expand Down Expand Up @@ -128,7 +128,7 @@ prompt_yn() {
# ------------------------------------------------------------------ sub-commands
cmd_help() {
cat <<USAGE
agentquest — fantasy village dashboard for Claude Code and Codex agents
agentquest — fantasy village dashboard for Claude Code, Codex and Cursor agents

USAGE:
agentquest [start] Start server + client, open browser
Expand Down
12 changes: 6 additions & 6 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Toasts, type ToastItem } from './components/Toasts';
import type { NotificationEntry } from './components/NotificationMenu';
import { useSettings } from './hooks/useSettings';
import { useAgentNotifications, type ToastPayload } from './hooks/useAgentNotifications';
import { isLiveRosterAgent } from './agentVisibility';
import './App.css';

export default function App() {
Expand Down Expand Up @@ -76,12 +77,11 @@ export default function App() {
? agents.find((a) => a.id === selectedAgentId) ?? null
: null;

// Only show source badges when both providers have a LIVE agent — completed
// / error sessions don't count, otherwise the badge would linger after the
// last Codex hero finishes just because it's still in state.
const liveAgents = agents.filter((a) => a.status !== 'completed' && a.status !== 'error');
const showSourceBadge = liveAgents.some((a) => a.source === 'claude')
&& liveAgents.some((a) => a.source === 'codex');
// Only show source badges when two or more providers have a LIVE agent —
// completed / error / parked waiting-idle sessions don't count.
const liveAgents = agents.filter((a) => isLiveRosterAgent(a));
const liveSources = new Set(liveAgents.map((a) => a.source));
const showSourceBadge = liveSources.size >= 2;

// When selecting agent, clear building
const handleSelectAgent = useCallback((id: string | null) => {
Expand Down
35 changes: 35 additions & 0 deletions client/src/agentVisibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect } from 'bun:test';
import { PARK_AFTER_MS, isParkedAgent, isLiveRosterAgent, isVillageVisibleAgent } from './agentVisibility';

const now = 1_000_000_000;

function agent(over: { status: 'active' | 'waiting' | 'idle' | 'completed' | 'error'; lastEvent: number }) {
return over;
}

describe('agentVisibility', () => {
it('parks waiting and idle after PARK_AFTER_MS, never parks active', () => {
expect(isParkedAgent(agent({ status: 'waiting', lastEvent: now - PARK_AFTER_MS - 1 }), now)).toBe(true);
expect(isParkedAgent(agent({ status: 'idle', lastEvent: now - PARK_AFTER_MS - 1 }), now)).toBe(true);
expect(isParkedAgent(agent({ status: 'waiting', lastEvent: now - 60_000 }), now)).toBe(false);
expect(isParkedAgent(agent({ status: 'active', lastEvent: now - PARK_AFTER_MS * 2 }), now)).toBe(false);
expect(isParkedAgent(agent({ status: 'completed', lastEvent: now - PARK_AFTER_MS * 2 }), now)).toBe(false);
});

it('keeps recent waiting/idle on the live roster', () => {
expect(isLiveRosterAgent(agent({ status: 'active', lastEvent: now }), now)).toBe(true);
expect(isLiveRosterAgent(agent({ status: 'waiting', lastEvent: now - 60_000 }), now)).toBe(true);
expect(isLiveRosterAgent(agent({ status: 'idle', lastEvent: now - 60_000 }), now)).toBe(true);
expect(isLiveRosterAgent(agent({ status: 'waiting', lastEvent: now - PARK_AFTER_MS - 1 }), now)).toBe(false);
expect(isLiveRosterAgent(agent({ status: 'completed', lastEvent: now }), now)).toBe(false);
expect(isLiveRosterAgent(agent({ status: 'error', lastEvent: now }), now)).toBe(false);
});

it('hides parked waiting/idle from the village along with completed/error', () => {
expect(isVillageVisibleAgent(agent({ status: 'waiting', lastEvent: now - 60_000 }), now)).toBe(true);
expect(isVillageVisibleAgent(agent({ status: 'waiting', lastEvent: now - PARK_AFTER_MS - 1 }), now)).toBe(false);
expect(isVillageVisibleAgent(agent({ status: 'idle', lastEvent: now - PARK_AFTER_MS - 1 }), now)).toBe(false);
expect(isVillageVisibleAgent(agent({ status: 'completed', lastEvent: now }), now)).toBe(false);
expect(isVillageVisibleAgent(agent({ status: 'error', lastEvent: now }), now)).toBe(false);
});
});
32 changes: 32 additions & 0 deletions client/src/agentVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { AgentState } from './types/agent';

/**
* Waiting / idle agents older than this leave the live roster (Party Bar,
* village, Top Bar counts, source badges). Matches CursorProvider's
* `endedQuietMs` so a finished turn that we still ingest doesn't linger
* as a waiting hero.
*/
export const PARK_AFTER_MS = 15 * 60 * 1000;

type StatusAndLastEvent = Pick<AgentState, 'status' | 'lastEvent'>;

/** Waiting or idle with no events for longer than {@link PARK_AFTER_MS}. */
export function isParkedAgent(agent: StatusAndLastEvent, now = Date.now()): boolean {
if (agent.status !== 'waiting' && agent.status !== 'idle') return false;
return now - agent.lastEvent > PARK_AFTER_MS;
}

/** Active, plus waiting/idle that are still within the park window. */
export function isLiveRosterAgent(agent: StatusAndLastEvent, now = Date.now()): boolean {
if (agent.status === 'active') return true;
if (agent.status === 'waiting' || agent.status === 'idle') {
return !isParkedAgent(agent, now);
}
return false;
}

/** Heroes that should walk the village (live roster minus completed/error). */
export function isVillageVisibleAgent(agent: StatusAndLastEvent, now = Date.now()): boolean {
if (agent.status === 'completed' || agent.status === 'error') return false;
return isLiveRosterAgent(agent, now);
}
2 changes: 1 addition & 1 deletion client/src/components/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function ActivityFeed({ log, agents, selectedAgentId, onSelectAgent, show
{filtered.length === 0 ? (
<div className="feed-empty">
<div>Waiting for agent activity...</div>
<div className="feed-empty-hint">Launch Claude Code or Codex in any project — it'll appear here.</div>
<div className="feed-empty-hint">Launch Claude Code, Codex or Cursor in any project — it'll appear here.</div>
</div>
) : (
filtered.map((entry) => {
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/BuildingInfoPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useLayoutEffect, useRef, useState } from 'react';
import type { AgentState } from '../types/agent';
import { isLiveRosterAgent } from '../agentVisibility';
import { BUILDING_DEFS } from '../game/data/building-layout';
import './BuildingInfoPanel.css';

Expand Down Expand Up @@ -45,7 +46,8 @@ export function BuildingInfoPanel({ buildingId, anchor, agents, onClose }: Build
if (building === undefined) return null;

const agentsHere = agents.filter(
(a) => a.currentActivity === building.activity && (a.status === 'active' || a.status === 'idle'),
(a) => a.currentActivity === building.activity && isLiveRosterAgent(a)
&& (a.status === 'active' || a.status === 'idle'),
);

const style = position === null
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/Minimap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useEffect } from 'react';
import type { AgentState } from '../types/agent';
import { isLiveRosterAgent } from '../agentVisibility';
import { BUILDING_DEFS, VILLAGE_GATE } from '../game/data/building-layout';
import './Minimap.css';

Expand Down Expand Up @@ -54,7 +55,9 @@ export function Minimap({ agents }: MinimapProps) {
ctx.lineWidth = 1;
ctx.strokeRect((VILLAGE_GATE.x - 15) * sx, (VILLAGE_GATE.y - 8) * sy, 30 * sx, 16 * sy);

const visible = agents.filter((a) => a.status === 'active' || a.status === 'idle');
const visible = agents.filter((a) =>
isLiveRosterAgent(a) && (a.status === 'active' || a.status === 'idle'),
);
for (const agent of visible) {
const building = BUILDING_DEFS.find((b) => b.activity === agent.currentActivity);
if (building === undefined) continue;
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/NoInstallBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Props {
* Full-width banner shown when the WebSocket is connected AND the server
* reported zero config dirs across both providers. Disambiguates the two
* empty-village states:
* - "Neither Claude Code nor Codex is installed" → this banner
* - "Neither Claude Code, Codex nor Cursor is installed" → this banner
* - "At least one provider is installed but idle" → no banner
* Dismiss is persisted (with one-time migration from the old Claude-only key)
* so returning users don't see it every load.
Expand Down Expand Up @@ -43,10 +43,10 @@ export function NoInstallBanner({ configDirs, connected }: Props) {
<div className="no-install-banner" role="status">
<span className="no-install-icon" aria-hidden>⚠</span>
<div className="no-install-body">
<div className="no-install-title">No Claude Code or Codex installation detected</div>
<div className="no-install-title">No Claude Code, Codex or Cursor installation detected</div>
<div className="no-install-text">
The server found no <code>~/.claude*</code> or <code>~/.codex</code> directory with session logs.
{' '}Start a Claude Code or Codex session to see heroes appear here.
The server found no <code>~/.claude*</code>, <code>~/.codex</code> or <code>~/.cursor</code> directory with session logs.
{' '}Start a Claude Code, Codex or Cursor session to see heroes appear here.
</div>
</div>
<button
Expand Down
Loading