Skip to content

Commit 1bb5f34

Browse files
feat: Improve Aider process monitoring and real-time output streaming
- Implemented real-time output streaming for Aider process - Added activity monitoring to detect and log long-running operations - Enhanced error handling and logging for Aider execution - Introduced progress indicators and more detailed console logging - Improved timeout and process management for Aider operations - Refined error messages and troubleshooting guidance
1 parent 3a202e1 commit 1bb5f34

File tree

1 file changed

+206
-83
lines changed

1 file changed

+206
-83
lines changed

src/patch.ts

Lines changed: 206 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -289,108 +289,234 @@ export class PatchClient {
289289
console.log('- OPENAI_API_KEY:', env.OPENAI_API_KEY ? 'Set' : 'Not set');
290290
console.log('- ANTHROPIC_API_KEY:', env.ANTHROPIC_API_KEY ? 'Set' : 'Not set');
291291

292-
// Run Aider with the issue file
293-
const { stdout, stderr } = await execa('aider', aiderArgs, {
294-
cwd: this.workingDir,
295-
timeout: this.options.timeout,
296-
env
297-
});
292+
// Run Aider with the issue file and stream output in real-time
293+
console.log('Starting Aider process - streaming output:');
294+
console.log('--------------------------------------------------');
298295

299-
// Log Aider output for debugging
300-
console.log('Aider stdout:', stdout);
301-
if (stderr) console.error('Aider stderr:', stderr);
296+
// Setup a check interval to detect if Aider is stuck
297+
let lastOutputTime = Date.now();
298+
let aiderActive = true;
299+
const activityInterval = setInterval(() => {
300+
const timeSinceLastOutput = Date.now() - lastOutputTime;
301+
if (aiderActive && timeSinceLastOutput > 30000) { // 30 seconds
302+
console.log(`[MONITOR] No output from Aider for ${Math.floor(timeSinceLastOutput/1000)} seconds. Still working...`);
303+
}
304+
}, 30000);
302305

303-
// Check if Aider made any changes
304-
const gitStatus = await execa('git', ['status', '--porcelain'], { cwd: this.workingDir });
305-
const hasChanges = gitStatus.stdout.trim().length > 0;
306+
let progressInterval: NodeJS.Timeout | null = null;
306307

307-
if (hasChanges) {
308-
// Get the list of changed files
309-
const changedFiles = gitStatus.stdout
310-
.split('\n')
311-
.filter(Boolean)
312-
.map(line => line.substring(3));
308+
try {
309+
// Use execa with streaming output
310+
const aiderProcess = execa('aider', aiderArgs, {
311+
cwd: this.workingDir,
312+
env,
313+
timeout: this.options.timeout,
314+
// Stream the output
315+
stdout: 'pipe',
316+
stderr: 'pipe',
317+
buffer: false // Important for real-time streaming
318+
});
319+
320+
// Stream stdout with timestamp updates
321+
if (aiderProcess.stdout) {
322+
aiderProcess.stdout.on('data', (data) => {
323+
const output = data.toString();
324+
if (output.trim()) {
325+
console.log(`[AIDER] ${output.trim()}`);
326+
lastOutputTime = Date.now();
327+
}
328+
});
329+
}
330+
331+
// Stream stderr
332+
if (aiderProcess.stderr) {
333+
aiderProcess.stderr.on('data', (data) => {
334+
const output = data.toString();
335+
if (output.trim()) {
336+
console.error(`[AIDER ERROR] ${output.trim()}`);
337+
lastOutputTime = Date.now();
338+
}
339+
});
340+
}
313341

314-
console.log(`Changes detected in ${changedFiles.length} files:`, changedFiles);
342+
// Start a progress indicator
343+
let progressDots = 0;
344+
progressInterval = setInterval(() => {
345+
if (aiderActive) {
346+
process.stdout.write('.');
347+
progressDots++;
348+
if (progressDots % 60 === 0) {
349+
process.stdout.write('\n');
350+
}
351+
}
352+
}, 5000);
353+
354+
// Wait for Aider to complete
355+
const { stdout, stderr } = await aiderProcess;
356+
357+
// Clear intervals once Aider is done
358+
clearInterval(activityInterval);
359+
if (progressInterval) clearInterval(progressInterval);
360+
aiderActive = false;
361+
362+
// Add a newline if we were printing dots
363+
if (progressDots > 0 && progressDots % 60 !== 0) {
364+
console.log('');
365+
}
315366

316-
// Commit the changes
317-
await execa('git', ['add', '.'], { cwd: this.workingDir });
318-
await execa('git', ['commit', '-m', `Fix: ${issueTitle}`], { cwd: this.workingDir });
367+
console.log('--------------------------------------------------');
368+
console.log('Aider process completed');
319369

320-
// Configure push URL with auth if needed
321-
if (authToken) {
322-
const url = new URL(repoUrl);
323-
// Use proper GitHub authentication format based on token type
324-
let authenticatedPushUrl;
370+
// Check if Aider made any changes
371+
console.log('Checking for changes made by Aider...');
372+
const gitStatus = await execa('git', ['status', '--porcelain'], { cwd: this.workingDir });
373+
const hasChanges = gitStatus.stdout.trim().length > 0;
374+
375+
if (hasChanges) {
376+
// Get the list of changed files
377+
const changedFiles = gitStatus.stdout
378+
.split('\n')
379+
.filter(Boolean)
380+
.map(line => line.substring(3));
381+
382+
console.log(`Changes detected in ${changedFiles.length} files:`, changedFiles);
383+
384+
// Commit the changes
385+
await execa('git', ['add', '.'], { cwd: this.workingDir });
386+
await execa('git', ['commit', '-m', `Fix: ${issueTitle}`], { cwd: this.workingDir });
325387

326-
if (url.hostname === 'github.com') {
327-
const tokenType = this.identifyTokenType(authToken);
388+
// Configure push URL with auth if needed
389+
if (authToken) {
390+
const url = new URL(repoUrl);
391+
// Use proper GitHub authentication format based on token type
392+
let authenticatedPushUrl;
328393

329-
if (tokenType === 'GitHub App Installation Token') {
330-
// For GitHub App installation tokens (ghs_*)
331-
authenticatedPushUrl = `https://x-access-token:${authToken}@github.com${url.pathname}`;
332-
} else if (tokenType === 'OAuth App Token') {
333-
// For OAuth tokens
334-
authenticatedPushUrl = `https://oauth2:${authToken}@github.com${url.pathname}`;
335-
} else if (tokenType === 'Personal Access Token' || tokenType === 'Fine-grained Personal Access Token') {
336-
// For PATs
337-
authenticatedPushUrl = `https://oauth2:${authToken}@github.com${url.pathname}`;
394+
if (url.hostname === 'github.com') {
395+
const tokenType = this.identifyTokenType(authToken);
396+
397+
if (tokenType === 'GitHub App Installation Token') {
398+
// For GitHub App installation tokens (ghs_*)
399+
authenticatedPushUrl = `https://x-access-token:${authToken}@github.com${url.pathname}`;
400+
} else if (tokenType === 'OAuth App Token') {
401+
// For OAuth tokens
402+
authenticatedPushUrl = `https://oauth2:${authToken}@github.com${url.pathname}`;
403+
} else if (tokenType === 'Personal Access Token' || tokenType === 'Fine-grained Personal Access Token') {
404+
// For PATs
405+
authenticatedPushUrl = `https://oauth2:${authToken}@github.com${url.pathname}`;
406+
} else {
407+
// Fallback for unknown token types - try x-access-token format
408+
authenticatedPushUrl = `https://x-access-token:${authToken}@github.com${url.pathname}`;
409+
console.log('Using x-access-token format for GitHub authentication');
410+
}
338411
} else {
339-
// Fallback for unknown token types - try x-access-token format
340-
authenticatedPushUrl = `https://x-access-token:${authToken}@github.com${url.pathname}`;
341-
console.log('Using x-access-token format for GitHub authentication');
412+
// For non-GitHub repositories
413+
authenticatedPushUrl = repoUrl.replace(`${url.protocol}//`, `${url.protocol}//${authToken}@`);
342414
}
343-
} else {
344-
// For non-GitHub repositories
345-
authenticatedPushUrl = repoUrl.replace(`${url.protocol}//`, `${url.protocol}//${authToken}@`);
415+
416+
// Apply the authenticated URL for push
417+
console.log(`Configuring authenticated remote for pushing to ${url.hostname}${url.pathname}`);
418+
await execa('git', ['remote', 'set-url', 'origin', authenticatedPushUrl], { cwd: this.workingDir });
346419
}
347420

348-
// Apply the authenticated URL for push
349-
console.log(`Configuring authenticated remote for pushing to ${url.hostname}${url.pathname}`);
350-
await execa('git', ['remote', 'set-url', 'origin', authenticatedPushUrl], { cwd: this.workingDir });
421+
// Push the changes
422+
console.log(`Pushing changes to branch: ${branchName}`);
423+
// Set environment variables for Git to prevent prompting
424+
const pushEnv = {
425+
...process.env,
426+
GIT_TERMINAL_PROMPT: '0',
427+
GIT_ASKPASS: 'echo',
428+
GCM_INTERACTIVE: 'never'
429+
};
430+
431+
try {
432+
await execa('git', ['push', 'origin', branchName], {
433+
cwd: this.workingDir,
434+
env: pushEnv,
435+
timeout: 60000 // 1 minute timeout for push
436+
});
437+
} catch (pushError) {
438+
const errorMessage = pushError instanceof Error ? pushError.message : String(pushError);
439+
console.error(`Push error (sanitized): ${this.sanitizeErrorMessage(errorMessage)}`);
440+
441+
if (errorMessage.includes('could not read Username') ||
442+
errorMessage.includes('Authentication failed') ||
443+
errorMessage.includes('403') ||
444+
errorMessage.includes('401')) {
445+
throw new Error('Failed to push changes: Authentication error. The token may not have write access to this repository.');
446+
}
447+
throw new Error(`Failed to push changes: ${this.sanitizeErrorMessage(errorMessage)}`);
448+
}
449+
450+
return {
451+
success: true,
452+
changes: changedFiles,
453+
message: 'Successfully applied fixes'
454+
};
455+
} else {
456+
console.log('No changes were made by Aider');
457+
return {
458+
success: false,
459+
changes: [],
460+
message: 'Aider did not make any changes to the codebase'
461+
};
351462
}
463+
} catch (error) {
464+
// Make sure to clean up intervals if there's an error
465+
clearInterval(activityInterval);
466+
if (progressInterval) clearInterval(progressInterval);
467+
aiderActive = false;
352468

353-
// Push the changes
354-
console.log(`Pushing changes to branch: ${branchName}`);
355-
// Set environment variables for Git to prevent prompting
356-
const pushEnv = {
357-
...process.env,
358-
GIT_TERMINAL_PROMPT: '0',
359-
GIT_ASKPASS: 'echo',
360-
GCM_INTERACTIVE: 'never'
361-
};
469+
console.error('Error running Aider:', error);
362470

363-
try {
364-
await execa('git', ['push', 'origin', branchName], {
365-
cwd: this.workingDir,
366-
env: pushEnv,
367-
timeout: 60000 // 1 minute timeout for push
368-
});
369-
} catch (pushError) {
370-
const errorMessage = pushError instanceof Error ? pushError.message : String(pushError);
371-
console.error(`Push error (sanitized): ${this.sanitizeErrorMessage(errorMessage)}`);
372-
373-
if (errorMessage.includes('could not read Username') ||
374-
errorMessage.includes('Authentication failed') ||
375-
errorMessage.includes('403') ||
376-
errorMessage.includes('401')) {
377-
throw new Error('Failed to push changes: Authentication error. The token may not have write access to this repository.');
471+
// Try to get additional information from the error
472+
let errorMessage = error instanceof Error ? error.message : String(error);
473+
474+
// Sanitize error message to remove any tokens
475+
errorMessage = this.sanitizeErrorMessage(errorMessage);
476+
477+
const fullError = errorMessage; // Keep the sanitized error for logging
478+
479+
// Log the sanitized error for debugging
480+
console.error('Full Aider error:', fullError);
481+
482+
// Check for specific error types
483+
if (errorMessage.includes('OPENAI_API_KEY') || errorMessage.includes('openai.error.AuthenticationError')) {
484+
if (this.isClaudeModel()) {
485+
errorMessage = 'API key error with Claude model. Trying these fixes:\n' +
486+
'1. Set both ANTHROPIC_API_KEY and a dummy OPENAI_API_KEY\n' +
487+
'2. Try different Claude flags: --anthropic, --claude, or --use-anthropic\n' +
488+
'3. Update to the latest version of Aider: pip install -U aider-chat';
489+
} else {
490+
errorMessage = 'OpenAI API key is missing or invalid. Please set the OPENAI_API_KEY environment variable.';
378491
}
379-
throw new Error(`Failed to push changes: ${this.sanitizeErrorMessage(errorMessage)}`);
492+
} else if (errorMessage.includes('ANTHROPIC_API_KEY') ||
493+
errorMessage.includes('anthropic.AuthenticationError') ||
494+
errorMessage.includes('anthropic.api_key')) {
495+
errorMessage = 'Anthropic API key is missing or invalid. Please set the ANTHROPIC_API_KEY environment variable.';
496+
} else if (errorMessage.includes('ENOENT') && errorMessage.includes('aider')) {
497+
errorMessage = 'Aider executable not found. Please install Aider with: pip install aider-chat';
498+
} else if (errorMessage.includes('ETIMEDOUT') || errorMessage.includes('timeout')) {
499+
errorMessage = `Aider operation timed out after ${this.options.timeout ? this.options.timeout / 1000 : 300} seconds`;
500+
} else if (errorMessage.includes('unrecognized arguments')) {
501+
errorMessage = `Aider command line error: ${errorMessage}\n\nThis may be due to version differences. Try updating Aider: pip install -U aider-chat`;
502+
}
503+
504+
// Log Claude-specific advice to console, but don't include it in the user-facing error message
505+
if (this.isClaudeModel()) {
506+
console.log('Additional Claude troubleshooting (for developers):');
507+
console.log('• Make sure ANTHROPIC_API_KEY is set and valid');
508+
console.log('• Try different flags for Claude in AIDER_EXTRA_ARGS (--anthropic or --claude)');
509+
console.log('• Check Aider version compatibility with Claude models');
380510
}
381511

382-
return {
383-
success: true,
384-
changes: changedFiles,
385-
message: 'Successfully applied fixes'
386-
};
387-
} else {
388-
console.log('No changes were made by Aider');
389512
return {
390513
success: false,
391514
changes: [],
392-
message: 'Aider did not make any changes to the codebase'
515+
message: `Error running Aider: ${errorMessage}`
393516
};
517+
} finally {
518+
// Clean up
519+
await this.cleanup();
394520
}
395521
} catch (error) {
396522
console.error('Error running Aider:', error);
@@ -441,9 +567,6 @@ export class PatchClient {
441567
changes: [],
442568
message: `Error running Aider: ${errorMessage}`
443569
};
444-
} finally {
445-
// Clean up
446-
await this.cleanup();
447570
}
448571
}
449572

0 commit comments

Comments
 (0)