fix: capture script now properly spawns subagents

- Fix input submission: add \r and useScreen:true for reliable input
- Link to real codebase instead of empty test case
- Select correct session tab before sending input
- Filter subagents by session for accurate tracking
- Screenshots now show real subagent windows with activity
- Optimized GIF to 3.9MB (was 62MB)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-01 07:16:02 +01:00
co-authored by Claude Opus 4.5
parent 2186d16616
commit dc0b9f0fb6
6 changed files with 98 additions and 26 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 220 KiB

+98 -26
View File
@@ -26,11 +26,15 @@ const OUTPUT_DIR = join(PROJECT_ROOT, 'docs', 'images');
const CASES_DIR = join(process.env.HOME, 'claudeman-cases'); const CASES_DIR = join(process.env.HOME, 'claudeman-cases');
// Configuration // Configuration
const PORT = 3199; // Unique port for capture script const PORT = 3198; // Unique port for capture script (avoid 3199 conflicts)
const BASE_URL = `http://localhost:${PORT}`; const BASE_URL = `http://localhost:${PORT}`;
const CASE_NAME = `screenshot-capture-${Date.now()}`; const CASE_NAME = `screenshot-capture-${Date.now()}`;
const VIEWPORT = { width: 1280, height: 720 }; const VIEWPORT = { width: 1280, height: 720 };
// Use linked case pointing to real codebase for actual code exploration
const USE_REAL_CODEBASE = true;
const REAL_CODEBASE_PATH = PROJECT_ROOT;
// Timeouts (generous for real Claude sessions) // Timeouts (generous for real Claude sessions)
const TIMEOUTS = { const TIMEOUTS = {
SERVER_STARTUP: 15000, SERVER_STARTUP: 15000,
@@ -74,7 +78,8 @@ async function startServer() {
serverProcess.stderr.on('data', (data) => { serverProcess.stderr.on('data', (data) => {
const msg = data.toString(); const msg = data.toString();
if (!msg.includes('DeprecationWarning')) { // Log errors but don't exit - let the startup check handle port conflicts
if (!msg.includes('DeprecationWarning') && !msg.includes('[RalphTracker]') && !msg.includes('[Server] Restored')) {
console.log('Server stderr:', msg.trim()); console.log('Server stderr:', msg.trim());
} }
}); });
@@ -145,6 +150,21 @@ async function createCase(name) {
return response.json(); return response.json();
} }
/**
* Link an existing folder as a case via API
*/
async function linkCase(name, folderPath) {
const response = await fetch(`${BASE_URL}/api/cases/link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, path: folderPath }),
});
if (!response.ok) {
throw new Error(`Failed to link case: ${await response.text()}`);
}
return response.json();
}
/** /**
* Quick-start a Claude session * Quick-start a Claude session
*/ */
@@ -172,9 +192,14 @@ async function deleteSession(sessionId) {
} }
/** /**
* Delete a case directory * Delete a case directory (only for non-linked cases)
*/ */
function deleteCase(caseName) { function deleteCase(caseName, isLinked = false) {
if (isLinked) {
// For linked cases, just log - the link will be cleaned up when server stops
console.log(`✓ Linked case ${caseName} will be unlinked`);
return;
}
const casePath = join(CASES_DIR, caseName); const casePath = join(CASES_DIR, caseName);
if (existsSync(casePath)) { if (existsSync(casePath)) {
try { try {
@@ -204,17 +229,34 @@ async function configureSettings(page) {
} }
/** /**
* Send input to a session * Send input to a session via API
* Note: useScreen requires \r at end to submit
*/ */
async function sendInput(sessionId, text) { async function sendInput(sessionId, text) {
const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/input`, { const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/input`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: text }), body: JSON.stringify({ input: text + '\r', useScreen: true }),
}); });
return response.ok; return response.ok;
} }
/**
* Get subagents for a specific session
*/
async function getSessionSubagents(sessionId) {
try {
const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/subagents`);
if (response.ok) {
const data = await response.json();
return data.data || [];
}
} catch {
// Ignore errors
}
return [];
}
/** /**
* Convert video to optimized GIF * Convert video to optimized GIF
*/ */
@@ -290,9 +332,14 @@ async function capture() {
console.log('Configuring settings...'); console.log('Configuring settings...');
await configureSettings(page); await configureSettings(page);
// Create case // Create or link case
console.log(`Creating case: ${CASE_NAME}...`); if (USE_REAL_CODEBASE) {
await createCase(CASE_NAME); console.log(`Linking case ${CASE_NAME} to ${REAL_CODEBASE_PATH}...`);
await linkCase(CASE_NAME, REAL_CODEBASE_PATH);
} else {
console.log(`Creating case: ${CASE_NAME}...`);
await createCase(CASE_NAME);
}
await page.reload({ waitUntil: 'domcontentloaded' }); await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(500); await sleep(500);
@@ -305,7 +352,7 @@ async function capture() {
if (!startResult.success) { if (!startResult.success) {
throw new Error(`Quick-start failed: ${startResult.error}`); throw new Error(`Quick-start failed: ${startResult.error}`);
} }
sessionId = startResult.session?.id; sessionId = startResult.sessionId;
console.log(`✓ Session started: ${sessionId}`); console.log(`✓ Session started: ${sessionId}`);
// Wait for session tab and terminal // Wait for session tab and terminal
@@ -313,9 +360,27 @@ async function capture() {
await page.waitForSelector('.xterm', { timeout: TIMEOUTS.SESSION_READY }); await page.waitForSelector('.xterm', { timeout: TIMEOUTS.SESSION_READY });
console.log('✓ Terminal ready'); console.log('✓ Terminal ready');
// Wait for Claude to initialize // IMPORTANT: Click on OUR session tab to ensure it's selected
// (other sessions from state.json may be present)
console.log('Selecting our session tab...');
const ourTab = page.locator(`.session-tab[data-id="${sessionId}"]`);
if (await ourTab.count() > 0) {
await ourTab.click();
console.log('✓ Selected our session tab');
} else {
// Fallback: click the last tab (most recently created)
const tabs = page.locator('.session-tab');
const count = await tabs.count();
if (count > 0) {
await tabs.nth(count - 1).click();
console.log('✓ Selected last session tab');
}
}
await sleep(1000);
// Wait for Claude to initialize (needs time to show prompt)
console.log('Waiting for Claude to initialize...'); console.log('Waiting for Claude to initialize...');
await sleep(5000); await sleep(10000);
// Screenshot 1: Initial state (before subagents) // Screenshot 1: Initial state (before subagents)
await page.screenshot({ await page.screenshot({
@@ -324,32 +389,39 @@ async function capture() {
}); });
console.log('✓ Saved: subagent-before.png'); console.log('✓ Saved: subagent-before.png');
// Focus terminal and send prompt // Send prompt via API (more reliable than keyboard simulation)
console.log('Sending prompt to trigger subagents...'); console.log('Sending prompt to trigger subagents via API...');
await page.click('.xterm'); const sent = await sendInput(sessionId, SUBAGENT_PROMPT);
await sleep(500); if (!sent) {
console.log('⚠ Failed to send input via API, trying keyboard...');
// Type the prompt character by character to simulate real input await page.click('.xterm');
for (const char of SUBAGENT_PROMPT) { await sleep(500);
await page.keyboard.type(char, { delay: 10 }); await page.keyboard.type(SUBAGENT_PROMPT, { delay: 5 });
await page.keyboard.press('Enter');
} else {
console.log('✓ Prompt sent via API');
} }
await page.keyboard.press('Enter');
// Wait for subagents to spawn // Give Claude time to start processing before polling
console.log('Waiting for Claude to start processing...');
await sleep(15000);
// Wait for subagents to spawn (filter to our session only)
console.log('Waiting for subagents to spawn...'); console.log('Waiting for subagents to spawn...');
const spawnStart = Date.now(); const spawnStart = Date.now();
let subagents = []; let subagents = [];
let spawnedCount = 0; let spawnedCount = 0;
while (Date.now() - spawnStart < TIMEOUTS.AGENT_SPAWN) { while (Date.now() - spawnStart < TIMEOUTS.AGENT_SPAWN) {
subagents = await pollSubagents(); // Use session-specific endpoint to only get our subagents
subagents = await getSessionSubagents(sessionId);
if (subagents.length >= 2) { if (subagents.length >= 2) {
console.log(`✓ Found ${subagents.length} subagents!`); console.log(`✓ Found ${subagents.length} subagents for our session!`);
spawnedCount = subagents.length; spawnedCount = subagents.length;
break; break;
} }
if (subagents.length > 0 && subagents.length !== spawnedCount) { if (subagents.length > 0 && subagents.length !== spawnedCount) {
console.log(` Found ${subagents.length} subagent(s)...`); console.log(` Found ${subagents.length} subagent(s) for our session...`);
spawnedCount = subagents.length; spawnedCount = subagents.length;
} }
await sleep(2000); await sleep(2000);
@@ -467,7 +539,7 @@ async function capture() {
await deleteSession(sessionId); await deleteSession(sessionId);
} }
deleteCase(CASE_NAME); deleteCase(CASE_NAME, USE_REAL_CODEBASE);
stopServer(); stopServer();
console.log('Done.'); console.log('Done.');