diff --git a/CLAUDE.md b/CLAUDE.md
index 831a7dca..73d70eee 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -290,6 +290,8 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| `scripts/postinstall.js` | npm postinstall hook for setup |
| `scripts/data-generator.sh` | Generate test data for development |
| `scripts/test-tail-links.sh` | Test clickable file links in tail output |
+| `scripts/capture-subagent-screenshots.mjs` | Capture subagent screenshots/GIFs for README (uses real Claude sessions) |
+| `scripts/mobile-screenshot.mjs` | Capture mobile UI screenshots |
## Memory Leak Prevention
diff --git a/README.md b/README.md
index 901f19c0..949dbecb 100644
--- a/README.md
+++ b/README.md
@@ -130,21 +130,13 @@ Claudeman detects and tracks Ralph Loops and Todos inside Claude Code:
**Watch your agents work in real-time.** Claudeman monitors Claude Code's background agents (the `Task` tool) and displays them in draggable floating windows with Matrix-style connection lines.
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Session Tab [AGENTS (3)] │
-│ │ │
-│ │ ╭──────────────────╮ ╭──────────────────╮ │
-│ ├─┤ Agent: explore │ │ Agent: implement │ │
-│ │ │ ● active │ │ ○ completed │ │
-│ │ │ Tool: Grep │ │ Result: success │ │
-│ │ ╰──────────────────╯ ╰──────────────────╯ │
-│ │ ╭──────────────────╮ │
-│ └─────────┤ Agent: test │ │
-│ │ ◐ idle │ │
-│ ╰──────────────────╯ │
-└─────────────────────────────────────────────────────────────┘
-```
+
+
+
+
+
+
+
**Features:**
- **Floating windows** — Draggable, resizable panels for each agent
diff --git a/docs/images/subagent-activity.png b/docs/images/subagent-activity.png
new file mode 100644
index 00000000..22d9834a
Binary files /dev/null and b/docs/images/subagent-activity.png differ
diff --git a/docs/images/subagent-before.png b/docs/images/subagent-before.png
new file mode 100644
index 00000000..6813c00e
Binary files /dev/null and b/docs/images/subagent-before.png differ
diff --git a/docs/images/subagent-demo.gif b/docs/images/subagent-demo.gif
new file mode 100644
index 00000000..aad6e923
Binary files /dev/null and b/docs/images/subagent-demo.gif differ
diff --git a/docs/images/subagent-multiple.png b/docs/images/subagent-multiple.png
new file mode 100644
index 00000000..12fc16a2
Binary files /dev/null and b/docs/images/subagent-multiple.png differ
diff --git a/docs/images/subagent-spawn.png b/docs/images/subagent-spawn.png
new file mode 100644
index 00000000..27fb1fe2
Binary files /dev/null and b/docs/images/subagent-spawn.png differ
diff --git a/package.json b/package.json
index 28f4e77d..abde4dcb 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,8 @@
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest run test/e2e/",
"test:e2e:quick": "vitest run test/e2e/workflows/quick-start.e2e.ts",
- "typecheck": "tsc --noEmit"
+ "typecheck": "tsc --noEmit",
+ "capture:subagents": "node scripts/capture-subagent-screenshots.mjs"
},
"keywords": [
"claude",
diff --git a/scripts/capture-subagent-screenshots.mjs b/scripts/capture-subagent-screenshots.mjs
new file mode 100755
index 00000000..1ba84f3a
--- /dev/null
+++ b/scripts/capture-subagent-screenshots.mjs
@@ -0,0 +1,488 @@
+#!/usr/bin/env node
+/**
+ * Capture screenshots and GIFs of Claudeman's subagent visualization
+ *
+ * Uses real Claude sessions that spawn actual subagents via the Task tool.
+ *
+ * Usage:
+ * node scripts/capture-subagent-screenshots.mjs
+ *
+ * Requires:
+ * - npx playwright install chromium
+ * - ffmpeg (for GIF conversion)
+ * - Claude CLI configured
+ * - Web server NOT running on port 3000 (script starts its own)
+ */
+
+import { chromium } from 'playwright';
+import { spawn, execSync } from 'child_process';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { mkdirSync, existsSync, unlinkSync, rmSync } from 'fs';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const PROJECT_ROOT = join(__dirname, '..');
+const OUTPUT_DIR = join(PROJECT_ROOT, 'docs', 'images');
+const CASES_DIR = join(process.env.HOME, 'claudeman-cases');
+
+// Configuration
+const PORT = 3199; // Unique port for capture script
+const BASE_URL = `http://localhost:${PORT}`;
+const CASE_NAME = `screenshot-capture-${Date.now()}`;
+const VIEWPORT = { width: 1280, height: 720 };
+
+// Timeouts (generous for real Claude sessions)
+const TIMEOUTS = {
+ SERVER_STARTUP: 15000,
+ SESSION_READY: 60000,
+ AGENT_SPAWN: 120000, // 2 minutes for agents to spawn
+ AGENT_ACTIVITY: 30000, // 30s for activity after spawn
+};
+
+// Prompt designed to trigger multiple parallel Task agents
+const SUBAGENT_PROMPT = `Explore this codebase in parallel:
+1. Use an Explore agent to find all TypeScript interfaces
+2. Use another Explore agent to understand the test structure
+3. Use a third agent to check the API endpoints
+
+Run all three agents in parallel and summarize findings.`;
+
+// Ensure output directory exists
+try { mkdirSync(OUTPUT_DIR, { recursive: true }); } catch {}
+
+let serverProcess = null;
+let browser = null;
+
+/**
+ * Start the Claudeman web server
+ */
+async function startServer() {
+ console.log(`Starting Claudeman server on port ${PORT}...`);
+
+ serverProcess = spawn('npx', ['tsx', 'src/index.ts', 'web', '--port', String(PORT)], {
+ cwd: PROJECT_ROOT,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: { ...process.env, NODE_ENV: 'development' },
+ });
+
+ serverProcess.stdout.on('data', (data) => {
+ const msg = data.toString();
+ if (msg.includes('error') || msg.includes('Error')) {
+ console.log('Server:', msg.trim());
+ }
+ });
+
+ serverProcess.stderr.on('data', (data) => {
+ const msg = data.toString();
+ if (!msg.includes('DeprecationWarning')) {
+ console.log('Server stderr:', msg.trim());
+ }
+ });
+
+ // Wait for server to be ready
+ const start = Date.now();
+ while (Date.now() - start < TIMEOUTS.SERVER_STARTUP) {
+ try {
+ const response = await fetch(`${BASE_URL}/api/status`);
+ if (response.ok) {
+ console.log('✓ Server ready');
+ return;
+ }
+ } catch {
+ // Not ready yet
+ }
+ await sleep(200);
+ }
+ throw new Error('Server failed to start within timeout');
+}
+
+/**
+ * Stop the server
+ */
+function stopServer() {
+ if (serverProcess) {
+ console.log('Stopping server...');
+ serverProcess.kill('SIGTERM');
+ serverProcess = null;
+ }
+}
+
+/**
+ * Sleep helper
+ */
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Poll for subagents via API
+ */
+async function pollSubagents() {
+ try {
+ const response = await fetch(`${BASE_URL}/api/subagents`);
+ if (response.ok) {
+ const data = await response.json();
+ return data.data || [];
+ }
+ } catch {
+ // Ignore errors
+ }
+ return [];
+}
+
+/**
+ * Create a case via API
+ */
+async function createCase(name) {
+ const response = await fetch(`${BASE_URL}/api/cases`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name }),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to create case: ${await response.text()}`);
+ }
+ return response.json();
+}
+
+/**
+ * Quick-start a Claude session
+ */
+async function quickStart(caseName) {
+ const response = await fetch(`${BASE_URL}/api/quick-start`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ caseName, mode: 'claude' }),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to quick-start: ${await response.text()}`);
+ }
+ return response.json();
+}
+
+/**
+ * Delete a session
+ */
+async function deleteSession(sessionId) {
+ try {
+ await fetch(`${BASE_URL}/api/sessions/${sessionId}`, { method: 'DELETE' });
+ } catch {
+ // Ignore errors
+ }
+}
+
+/**
+ * Delete a case directory
+ */
+function deleteCase(caseName) {
+ const casePath = join(CASES_DIR, caseName);
+ if (existsSync(casePath)) {
+ try {
+ rmSync(casePath, { recursive: true, force: true });
+ console.log(`✓ Cleaned up case: ${caseName}`);
+ } catch (err) {
+ console.log(`Warning: Could not delete case: ${err.message}`);
+ }
+ }
+}
+
+/**
+ * Configure localStorage settings for subagent visibility
+ */
+async function configureSettings(page) {
+ await page.evaluate(() => {
+ const settings = {
+ showSubagents: true,
+ subagentTrackingEnabled: true,
+ subagentActiveTabOnly: false, // Show all subagents regardless of active tab
+ showMonitor: true,
+ };
+ localStorage.setItem('claudeman-app-settings', JSON.stringify(settings));
+ });
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await sleep(500);
+}
+
+/**
+ * Send input to a session
+ */
+async function sendInput(sessionId, text) {
+ const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/input`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ input: text }),
+ });
+ return response.ok;
+}
+
+/**
+ * Convert video to optimized GIF
+ */
+function convertToGif(inputPath, outputPath) {
+ console.log('Converting video to GIF...');
+ const palettePath = join(OUTPUT_DIR, 'palette.png');
+
+ try {
+ // Generate palette for better quality
+ execSync(`ffmpeg -y -i "${inputPath}" -vf "fps=15,scale=1280:-1:flags=lanczos,palettegen" "${palettePath}"`, {
+ stdio: 'pipe',
+ });
+
+ // Create GIF using palette
+ execSync(`ffmpeg -y -i "${inputPath}" -i "${palettePath}" -lavfi "fps=15,scale=1280:-1:flags=lanczos[x];[x][1:v]paletteuse" "${outputPath}"`, {
+ stdio: 'pipe',
+ });
+
+ // Clean up palette
+ if (existsSync(palettePath)) {
+ unlinkSync(palettePath);
+ }
+
+ console.log(`✓ GIF saved: ${outputPath}`);
+ } catch (err) {
+ console.error('Failed to convert to GIF:', err.message);
+ throw err;
+ }
+}
+
+/**
+ * Main capture flow
+ */
+async function capture() {
+ console.log('='.repeat(60));
+ console.log('Claudeman Subagent Screenshot Capture');
+ console.log('='.repeat(60));
+ console.log(`Output directory: ${OUTPUT_DIR}`);
+ console.log(`Case name: ${CASE_NAME}`);
+ console.log('');
+
+ let sessionId = null;
+
+ try {
+ // Start server
+ await startServer();
+
+ // Launch browser
+ console.log('Launching browser...');
+ browser = await chromium.launch({
+ headless: true,
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-gpu',
+ ],
+ });
+
+ const context = await browser.newContext({
+ viewport: VIEWPORT,
+ recordVideo: { dir: OUTPUT_DIR, size: VIEWPORT },
+ });
+ const page = await context.newPage();
+ page.setDefaultTimeout(30000);
+
+ // Navigate to Claudeman
+ console.log('Loading Claudeman...');
+ await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
+ await sleep(1000);
+
+ // Configure settings for subagent visibility
+ console.log('Configuring settings...');
+ await configureSettings(page);
+
+ // Create case
+ console.log(`Creating case: ${CASE_NAME}...`);
+ await createCase(CASE_NAME);
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await sleep(500);
+
+ // Select case in dropdown
+ await page.selectOption('#quickStartCase', CASE_NAME);
+
+ // Quick-start session
+ console.log('Starting Claude session...');
+ const startResult = await quickStart(CASE_NAME);
+ if (!startResult.success) {
+ throw new Error(`Quick-start failed: ${startResult.error}`);
+ }
+ sessionId = startResult.session?.id;
+ console.log(`✓ Session started: ${sessionId}`);
+
+ // Wait for session tab and terminal
+ await page.waitForSelector('.session-tab', { timeout: TIMEOUTS.SESSION_READY });
+ await page.waitForSelector('.xterm', { timeout: TIMEOUTS.SESSION_READY });
+ console.log('✓ Terminal ready');
+
+ // Wait for Claude to initialize
+ console.log('Waiting for Claude to initialize...');
+ await sleep(5000);
+
+ // Screenshot 1: Initial state (before subagents)
+ await page.screenshot({
+ path: join(OUTPUT_DIR, 'subagent-before.png'),
+ fullPage: false,
+ });
+ console.log('✓ Saved: subagent-before.png');
+
+ // Focus terminal and send prompt
+ console.log('Sending prompt to trigger subagents...');
+ await page.click('.xterm');
+ await sleep(500);
+
+ // Type the prompt character by character to simulate real input
+ for (const char of SUBAGENT_PROMPT) {
+ await page.keyboard.type(char, { delay: 10 });
+ }
+ await page.keyboard.press('Enter');
+
+ // Wait for subagents to spawn
+ console.log('Waiting for subagents to spawn...');
+ const spawnStart = Date.now();
+ let subagents = [];
+ let spawnedCount = 0;
+
+ while (Date.now() - spawnStart < TIMEOUTS.AGENT_SPAWN) {
+ subagents = await pollSubagents();
+ if (subagents.length >= 2) {
+ console.log(`✓ Found ${subagents.length} subagents!`);
+ spawnedCount = subagents.length;
+ break;
+ }
+ if (subagents.length > 0 && subagents.length !== spawnedCount) {
+ console.log(` Found ${subagents.length} subagent(s)...`);
+ spawnedCount = subagents.length;
+ }
+ await sleep(2000);
+ }
+
+ if (subagents.length === 0) {
+ console.log('⚠ No subagents spawned within timeout');
+ console.log(' This may happen if Claude chose not to use parallel agents');
+ console.log(' Capturing current state anyway...');
+ }
+
+ // Wait a bit more for windows to open and activity to appear
+ if (subagents.length > 0) {
+ console.log('Waiting for agent activity...');
+ await sleep(5000);
+
+ // Check if subagent windows are visible
+ const windowCount = await page.locator('.subagent-window').count();
+ console.log(` Subagent windows visible: ${windowCount}`);
+
+ // If windows aren't auto-opening, try to open them manually
+ if (windowCount < subagents.length) {
+ console.log(' Opening subagent windows...');
+ for (const agent of subagents) {
+ await page.evaluate((agentId) => {
+ if (window.app && window.app.openSubagentWindow) {
+ window.app.openSubagentWindow(agentId);
+ }
+ }, agent.agentId);
+ await sleep(500);
+ }
+ }
+ }
+
+ // Wait for activity in windows
+ await sleep(3000);
+
+ // Screenshot 2: Agents spawned (the main screenshot)
+ await page.screenshot({
+ path: join(OUTPUT_DIR, 'subagent-spawn.png'),
+ fullPage: false,
+ });
+ console.log('✓ Saved: subagent-spawn.png');
+
+ // If we have multiple agents, capture that state
+ if (subagents.length >= 2) {
+ await page.screenshot({
+ path: join(OUTPUT_DIR, 'subagent-multiple.png'),
+ fullPage: false,
+ });
+ console.log('✓ Saved: subagent-multiple.png');
+ }
+
+ // Wait for more activity and capture activity screenshot
+ console.log('Waiting for tool activity...');
+ await sleep(TIMEOUTS.AGENT_ACTIVITY);
+
+ await page.screenshot({
+ path: join(OUTPUT_DIR, 'subagent-activity.png'),
+ fullPage: false,
+ });
+ console.log('✓ Saved: subagent-activity.png');
+
+ // Close context to save video
+ console.log('Saving video recording...');
+ await context.close();
+
+ // Find the recorded video
+ const videoPath = await page.video()?.path();
+ if (videoPath && existsSync(videoPath)) {
+ const gifPath = join(OUTPUT_DIR, 'subagent-demo.gif');
+ convertToGif(videoPath, gifPath);
+
+ // Clean up video file
+ try {
+ unlinkSync(videoPath);
+ } catch {
+ // Ignore
+ }
+ } else {
+ console.log('⚠ Video recording not found');
+ }
+
+ console.log('');
+ console.log('='.repeat(60));
+ console.log('Capture complete!');
+ console.log('='.repeat(60));
+ console.log('');
+ console.log('Screenshots saved to:', OUTPUT_DIR);
+ console.log('');
+ console.log('Files:');
+ console.log(' - subagent-before.png (initial state)');
+ console.log(' - subagent-spawn.png (agents appearing)');
+ if (subagents.length >= 2) {
+ console.log(' - subagent-multiple.png (multiple agents)');
+ }
+ console.log(' - subagent-activity.png (tool activity)');
+ console.log(' - subagent-demo.gif (animated demo)');
+
+ } catch (err) {
+ console.error('');
+ console.error('Error during capture:', err.message);
+ console.error(err.stack);
+ process.exitCode = 1;
+ } finally {
+ // Cleanup
+ console.log('');
+ console.log('Cleaning up...');
+
+ if (browser) {
+ await browser.close().catch(() => {});
+ }
+
+ if (sessionId) {
+ await deleteSession(sessionId);
+ }
+
+ deleteCase(CASE_NAME);
+ stopServer();
+
+ console.log('Done.');
+ }
+}
+
+// Handle interrupts
+process.on('SIGINT', () => {
+ console.log('\nInterrupted, cleaning up...');
+ stopServer();
+ if (browser) {
+ browser.close().catch(() => {});
+ }
+ process.exit(1);
+});
+
+// Run
+capture();