Files
Codeman/scripts/capture-subagent-screenshots.mjs
T
arkonandClaude Opus 4.5 cc7c188f1f feat: add subagent screenshot capture script and README images
- Add scripts/capture-subagent-screenshots.mjs for automated capture
- Creates real Claude sessions that spawn Task tool subagents
- Records video and converts to optimized GIF via FFmpeg
- Add subagent-spawn.png, subagent-demo.gif to docs/images
- Update README Live Agent Visualization with real screenshots
- Replace ASCII diagram with actual subagent window screenshots
- Add capture:subagents npm script
- Document both screenshot scripts in CLAUDE.md

Commit from flight QR118 in an Airbus A350-1000, stable connection thanks to Starlink!

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 00:35:18 +01:00

489 lines
13 KiB
JavaScript
Executable File

#!/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();