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>
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 220 KiB |
@@ -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.');
|
||||||
|
|||||||