Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions apps/frontend/src/main/ipc-handlers/terminal-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function registerTerminalHandlers(
(_, id: string, cwd?: string) => {
// Use async version to avoid blocking main process during CLI detection
terminalManager.invokeClaudeAsync(id, cwd).catch((error) => {
console.error('[terminal-handlers] Failed to invoke Claude:', error);
debugError('[terminal-handlers] Failed to invoke Claude:', error);
});
}
);
Expand Down Expand Up @@ -312,12 +312,17 @@ export function registerTerminalHandlers(
ipcMain.handle(
IPC_CHANNELS.CLAUDE_PROFILE_INITIALIZE,
async (_, profileId: string): Promise<IPCResult> => {
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Handler called for profileId:', profileId);
try {
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile manager...');
const profileManager = getClaudeProfileManager();
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile...');
const profile = profileManager.getProfile(profileId);
if (!profile) {
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile not found!');
return { success: false, error: 'Profile not found' };
}
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile found:', profile.name);

// Ensure the config directory exists for non-default profiles
if (!profile.isDefault && profile.configDir) {
Expand All @@ -333,6 +338,7 @@ export function registerTerminalHandlers(
const terminalId = `claude-login-${profileId}-${Date.now()}`;
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';

debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Creating terminal:', terminalId);
debugLog('[IPC] Initializing Claude profile:', {
profileId,
profileName: profile.name,
Expand All @@ -341,7 +347,9 @@ export function registerTerminalHandlers(
});

// Create a new terminal for the login process
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Calling terminalManager.create...');
const createResult = await terminalManager.create({ id: terminalId, cwd: homeDir });
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Terminal created:', createResult.success);

// If terminal creation failed, return the error
if (!createResult.success) {
Expand All @@ -352,17 +360,18 @@ export function registerTerminalHandlers(
}

// Wait a moment for the terminal to initialize
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Waiting 500ms for terminal init...');
await new Promise(resolve => setTimeout(resolve, 500));
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Wait complete');

// Build the login command with the profile's config dir
// Use platform-specific syntax and escaping for environment variables
// Use full path to claude CLI - no need to modify PATH since we have the absolute path
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting Claude CLI invocation...');
let loginCommand: string;
const { command: claudeCmd, env: claudeEnv } = await getClaudeCliInvocationAsync();
const pathPrefix = claudeEnv.PATH
? (process.platform === 'win32'
? `set "PATH=${escapeShellArgWindows(claudeEnv.PATH)}" && `
: `export PATH=${escapeShellArg(claudeEnv.PATH)} && `)
: '';
const { command: claudeCmd } = await getClaudeCliInvocationAsync();
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Got Claude CLI:', claudeCmd);

// Use the full path directly - escaping only needed for paths with spaces
const shellClaudeCmd = process.platform === 'win32'
? `"${escapeShellArgWindows(claudeCmd)}"`
: escapeShellArg(claudeCmd);
Expand All @@ -372,24 +381,30 @@ export function registerTerminalHandlers(
// SECURITY: Use Windows-specific escaping for cmd.exe
const escapedConfigDir = escapeShellArgWindows(profile.configDir);
// Windows cmd.exe syntax: set "VAR=value" with %VAR% for expansion
loginCommand = `${pathPrefix}set "CLAUDE_CONFIG_DIR=${escapedConfigDir}" && echo Config dir: %CLAUDE_CONFIG_DIR% && ${shellClaudeCmd} setup-token`;
loginCommand = `set "CLAUDE_CONFIG_DIR=${escapedConfigDir}" && echo Config dir: %CLAUDE_CONFIG_DIR% && ${shellClaudeCmd} setup-token`;
} else {
// SECURITY: Use POSIX escaping for bash/zsh
const escapedConfigDir = escapeShellArg(profile.configDir);
// Unix/Mac bash/zsh syntax: export VAR=value with $VAR for expansion
loginCommand = `${pathPrefix}export CLAUDE_CONFIG_DIR=${escapedConfigDir} && echo "Config dir: $CLAUDE_CONFIG_DIR" && ${shellClaudeCmd} setup-token`;
loginCommand = `export CLAUDE_CONFIG_DIR=${escapedConfigDir} && echo "Config dir: $CLAUDE_CONFIG_DIR" && ${shellClaudeCmd} setup-token`;
}
} else {
loginCommand = `${pathPrefix}${shellClaudeCmd} setup-token`;
// Simple command for default profile - just run setup-token
loginCommand = `${shellClaudeCmd} setup-token`;
}

debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Built login command, length:', loginCommand.length);
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Login command:', loginCommand);
debugLog('[IPC] Sending login command to terminal:', loginCommand);

// Write the login command to the terminal
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Writing command to terminal...');
terminalManager.write(terminalId, `${loginCommand}\r`);
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Command written successfully');

// Notify the renderer that an auth terminal was created
// This allows the UI to display the terminal so users can see the OAuth flow
debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Notifying renderer of auth terminal...');
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.TERMINAL_AUTH_CREATED, {
Expand All @@ -399,6 +414,7 @@ export function registerTerminalHandlers(
});
}

debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Returning success!');
return {
success: true,
data: {
Expand All @@ -407,7 +423,7 @@ export function registerTerminalHandlers(
}
};
} catch (error) {
debugError('[IPC] Failed to initialize Claude profile:', error);
debugError('[IPC:CLAUDE_PROFILE_INITIALIZE] EXCEPTION:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to initialize Claude profile'
Expand Down Expand Up @@ -640,7 +656,7 @@ export function registerTerminalHandlers(
(_, id: string, sessionId?: string) => {
// Use async version to avoid blocking main process during CLI detection
terminalManager.resumeClaudeAsync(id, sessionId).catch((error) => {
console.error('[terminal-handlers] Failed to resume Claude:', error);
debugError('[terminal-handlers] Failed to resume Claude:', error);
});
}
);
Expand All @@ -651,7 +667,7 @@ export function registerTerminalHandlers(
IPC_CHANNELS.TERMINAL_ACTIVATE_DEFERRED_RESUME,
(_, id: string) => {
terminalManager.activateDeferredResume(id).catch((error) => {
console.error('[terminal-handlers] Failed to activate deferred Claude resume:', error);
debugError('[terminal-handlers] Failed to activate deferred Claude resume:', error);
});
}
);
Expand Down
93 changes: 90 additions & 3 deletions apps/frontend/src/main/terminal/pty-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { TerminalProcess, WindowGetter } from './types';
import { IPC_CHANNELS } from '../../shared/constants';
import { getClaudeProfileManager } from '../claude-profile-manager';
import { readSettingsFile } from '../settings-utils';
import { debugLog, debugError } from '../../shared/utils/debug-logger';
import type { SupportedTerminal } from '../../shared/types/settings';

/**
Expand Down Expand Up @@ -84,7 +85,7 @@ export function spawnPtyProcess(

const shellArgs = process.platform === 'win32' ? [] : ['-l'];

console.warn('[PtyManager] Spawning shell:', shell, shellArgs, '(preferred:', preferredTerminal || 'system', ')');
debugLog('[PtyManager] Spawning shell:', shell, shellArgs, '(preferred:', preferredTerminal || 'system', ')');

// Create a clean environment without DEBUG to prevent Claude Code from
// enabling debug mode when the Electron app is run in development mode.
Expand Down Expand Up @@ -137,7 +138,7 @@ export function setupPtyHandlers(

// Handle terminal exit
ptyProcess.onExit(({ exitCode }) => {
console.warn('[PtyManager] Terminal exited:', id, 'code:', exitCode);
debugLog('[PtyManager] Terminal exited:', id, 'code:', exitCode);

const win = getWindow();
if (win) {
Expand All @@ -151,11 +152,97 @@ export function setupPtyHandlers(
});
}

/**
* Constants for chunked write behavior
* CHUNKED_WRITE_THRESHOLD: Data larger than this (bytes) will be written in chunks
* CHUNK_SIZE: Size of each chunk - smaller chunks yield to event loop more frequently
*/
const CHUNKED_WRITE_THRESHOLD = 1000;
const CHUNK_SIZE = 100;

Comment on lines +155 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for CHUNK_SIZE and chunked write related references
echo "=== Searching for CHUNK_SIZE and chunked write references ==="
rg -n "CHUNK_SIZE|chunked.*write|CHUNKED_WRITE" --type=ts --type=tsx --type=md -C 2

Repository: AndyMik90/Auto-Claude

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Check for performance tests or benchmarks
echo "=== Looking for performance tests or benchmarks ==="
fd -e test.ts -e test.tsx -e spec.ts -e bench.ts | xargs rg -l "chunk|pty|terminal|write" 2>/dev/null || echo "No test files found with those patterns"

Repository: AndyMik90/Auto-Claude

Length of output: 3092


🏁 Script executed:

#!/bin/bash
# Get the full context of pty-manager.ts around the constants and their usage
echo "=== Examining pty-manager.ts file size ==="
wc -l apps/frontend/src/main/terminal/pty-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Read the pty-manager.ts file to understand context and usage
echo "=== Reading pty-manager.ts ==="
cat -n apps/frontend/src/main/terminal/pty-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 9408


🏁 Script executed:

#!/bin/bash
# Simpler search for CHUNK_SIZE and related constants
echo "=== Searching for CHUNK_SIZE usage ==="
rg -n "CHUNK_SIZE|CHUNKED_WRITE_THRESHOLD|setImmediate" apps/frontend/src/main/terminal/ -C 3

Repository: AndyMik90/Auto-Claude

Length of output: 4038


The chunked write approach is sound, though the rationale for specific chunk size could be clearer.

The constants are well-extracted and documented. However, the logic is intentionally designed to yield to the event loop via setImmediate between chunks—this is not a latency concern but the intended behavior to prevent blocking. CHUNK_SIZE of 100 bytes is conservative by design for this purpose, and the 1000-byte threshold ensures small writes bypass chunking entirely. While performance testing could validate whether the chunk size is optimal for actual throughput, it is not necessary for correctness, as the current values are reasonable for a terminal I/O operation.

🤖 Prompt for AI Agents
In @apps/frontend/src/main/terminal/pty-manager.ts around lines 155 - 162,
Update the block comment above CHUNKED_WRITE_THRESHOLD and CHUNK_SIZE to
explicitly state that chunking uses setImmediate between chunks to intentionally
yield to the event loop (to avoid blocking), that CHUNK_SIZE = 100 is a
conservative value chosen for yielding rather than latency/throughput
optimization, and that CHUNKED_WRITE_THRESHOLD = 1000 ensures small writes
bypass chunking; note that performance tuning is optional and not required for
correctness.

/**
* Write queue per terminal to prevent interleaving of concurrent writes.
* Maps terminal ID to the last write Promise in the queue.
*/
const pendingWrites = new Map<string, Promise<void>>();

/**
* Internal function to perform the actual write (chunked or direct)
* Returns a Promise that resolves when the write is complete
*/
function performWrite(terminal: TerminalProcess, data: string): Promise<void> {
return new Promise((resolve) => {
// For large commands, write in chunks to prevent blocking
if (data.length > CHUNKED_WRITE_THRESHOLD) {
debugLog('[PtyManager:writeToPty] Large write detected, using chunked write');
let offset = 0;
let chunkNum = 0;

const writeChunk = () => {
// Check if terminal is still valid before writing
if (!terminal.pty) {
debugError('[PtyManager:writeToPty] Terminal PTY no longer valid, aborting chunked write');
resolve();
return;
}

if (offset >= data.length) {
debugLog('[PtyManager:writeToPty] Chunked write completed, total chunks:', chunkNum);
resolve();
return;
}

const chunk = data.slice(offset, offset + CHUNK_SIZE);
chunkNum++;
try {
terminal.pty.write(chunk);
offset += CHUNK_SIZE;
// Use setImmediate to yield to the event loop between chunks
setImmediate(writeChunk);
} catch (error) {
debugError('[PtyManager:writeToPty] Chunked write FAILED at chunk', chunkNum, ':', error);
resolve(); // Resolve anyway - fire-and-forget semantics
}
};

// Start the chunked write after yielding
setImmediate(writeChunk);
} else {
try {
terminal.pty.write(data);
debugLog('[PtyManager:writeToPty] Write completed successfully');
} catch (error) {
debugError('[PtyManager:writeToPty] Write FAILED:', error);
}
resolve();
}
});
}

/**
* Write data to a PTY process
* Uses setImmediate to prevent blocking the event loop on large writes.
* Serializes writes per terminal to prevent interleaving of concurrent writes.
*/
export function writeToPty(terminal: TerminalProcess, data: string): void {
terminal.pty.write(data);
debugLog('[PtyManager:writeToPty] About to write to pty, data length:', data.length);

// Get the previous write Promise for this terminal (if any)
const previousWrite = pendingWrites.get(terminal.id) || Promise.resolve();

// Chain this write after the previous one completes
const currentWrite = previousWrite.then(() => performWrite(terminal, data));

// Update the pending write for this terminal
pendingWrites.set(terminal.id, currentWrite);

// Clean up the Map entry when done to prevent memory leaks
currentWrite.finally(() => {
// Only clean up if this is still the latest write
if (pendingWrites.get(terminal.id) === currentWrite) {
pendingWrites.delete(terminal.id);
}
});
}
Comment on lines +155 to 246
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix “bytes” vs string.length mismatch; harden the write queue against unexpected rejection.

  • Line 157 says “bytes” but the implementation uses data.length (UTF-16 code units). Either adjust the comment or use Buffer.byteLength(data, 'utf8') for thresholding (and reconsider chunking-by-bytes if you go that route).
  • Consider making the queue resilient if anything ever rejects (future refactors): previousWrite.catch(() => {}).then(...), otherwise one rejection can “poison” the chain for that terminal.
  • Minor: prefer offset += chunk.length instead of offset += CHUNK_SIZE for clarity/correctness at the tail chunk.
Proposed diff
- * CHUNKED_WRITE_THRESHOLD: Data larger than this (bytes) will be written in chunks
+ * CHUNKED_WRITE_THRESHOLD: Data larger than this (string length) will be written in chunks
  * CHUNK_SIZE: Size of each chunk - smaller chunks yield to event loop more frequently
 export function writeToPty(terminal: TerminalProcess, data: string): void {
   debugLog('[PtyManager:writeToPty] About to write to pty, data length:', data.length);

   // Get the previous write Promise for this terminal (if any)
   const previousWrite = pendingWrites.get(terminal.id) || Promise.resolve();

   // Chain this write after the previous one completes
-  const currentWrite = previousWrite.then(() => performWrite(terminal, data));
+  const currentWrite = previousWrite
+    .catch(() => undefined)
+    .then(() => performWrite(terminal, data));
-        const chunk = data.slice(offset, offset + CHUNK_SIZE);
+        const chunk = data.slice(offset, offset + CHUNK_SIZE);
         chunkNum++;
         try {
           terminal.pty.write(chunk);
-          offset += CHUNK_SIZE;
+          offset += chunk.length;
           // Use setImmediate to yield to the event loop between chunks
           setImmediate(writeChunk);
🤖 Prompt for AI Agents
In @apps/frontend/src/main/terminal/pty-manager.ts around lines 155 - 246, The
threshold comment and logic mismatch: CHUNKED_WRITE_THRESHOLD currently compares
UTF-16 code units (data.length) not bytes — update the comment to say “UTF-16
code units” or change the check to use Buffer.byteLength(data, 'utf8') and, if
chunking by bytes, adjust chunk slicing accordingly; inside performWrite replace
offset += CHUNK_SIZE with offset += chunk.length to handle the final partial
chunk correctly; harden the write queue in writeToPty by preventing a poisoned
chain: derive previousWrite with const previousWrite =
(pendingWrites.get(terminal.id) || Promise.resolve()).catch(() => {}) so any
rejection is swallowed before .then(() => performWrite(...)); keep references:
CHUNKED_WRITE_THRESHOLD, CHUNK_SIZE, performWrite, writeToPty, and
pendingWrites.


/**
Expand Down
10 changes: 8 additions & 2 deletions apps/frontend/src/main/terminal/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as SessionHandler from './session-handler';
import * as TerminalLifecycle from './terminal-lifecycle';
import * as TerminalEventHandler from './terminal-event-handler';
import * as ClaudeIntegration from './claude-integration-handler';
import { debugLog, debugError } from '../../shared/utils/debug-logger';

export class TerminalManager {
private terminals: Map<string, TerminalProcess> = new Map();
Expand Down Expand Up @@ -84,7 +85,7 @@ export class TerminalManager {
onResumeNeeded: (terminalId, sessionId) => {
// Use async version to avoid blocking main process
this.resumeClaudeAsync(terminalId, sessionId).catch((error) => {
console.error('[terminal-manager] Failed to resume Claude session:', error);
debugError('[terminal-manager] Failed to resume Claude session:', error);
});
}
},
Expand Down Expand Up @@ -120,9 +121,14 @@ export class TerminalManager {
* Send input to a terminal
*/
write(id: string, data: string): void {
debugLog('[TerminalManager:write] Writing to terminal:', id, 'data length:', data.length);
const terminal = this.terminals.get(id);
if (terminal) {
debugLog('[TerminalManager:write] Terminal found, calling writeToPty...');
PtyManager.writeToPty(terminal, data);
debugLog('[TerminalManager:write] writeToPty completed');
} else {
debugError('[TerminalManager:write] Terminal NOT found:', id);
}
}

Expand Down Expand Up @@ -311,7 +317,7 @@ export class TerminalManager {
onResumeNeeded: (terminalId, sessionId) => {
// Use async version to avoid blocking main process
this.resumeClaudeAsync(terminalId, sessionId).catch((error) => {
console.error('[terminal-manager] Failed to resume Claude session:', error);
debugError('[terminal-manager] Failed to resume Claude session:', error);
});
}
},
Expand Down
35 changes: 21 additions & 14 deletions apps/frontend/src/renderer/components/RateLimitModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ import { Label } from './ui/label';
import { Input } from './ui/input';
import { useRateLimitStore } from '../stores/rate-limit-store';
import { useClaudeProfileStore, loadClaudeProfiles, switchTerminalToProfile } from '../stores/claude-profile-store';
import { useToast } from '../hooks/use-toast';
import { debugError } from '../../shared/utils/debug-logger';

const CLAUDE_UPGRADE_URL = 'https://claude.ai/upgrade';

export function RateLimitModal() {
const { t } = useTranslation('common');
const { isModalOpen, rateLimitInfo, hideRateLimitModal, clearPendingRateLimit } = useRateLimitStore();
const { profiles, activeProfileId, isSwitching } = useClaudeProfileStore();
const { toast } = useToast();
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(null);
const [autoSwitchEnabled, setAutoSwitchEnabled] = useState(false);
const [isLoadingSettings, setIsLoadingSettings] = useState(false);
Expand Down Expand Up @@ -64,7 +67,7 @@ export function RateLimitModal() {
setAutoSwitchEnabled(result.data.autoSwitchOnRateLimit);
}
} catch (err) {
console.error('Failed to load auto-switch settings:', err);
debugError('[RateLimitModal] Failed to load auto-switch settings:', err);
}
};

Expand All @@ -77,7 +80,7 @@ export function RateLimitModal() {
});
setAutoSwitchEnabled(enabled);
} catch (err) {
console.error('Failed to update auto-switch settings:', err);
debugError('[RateLimitModal] Failed to update auto-switch settings:', err);
} finally {
setIsLoadingSettings(false);
}
Expand Down Expand Up @@ -116,22 +119,26 @@ export function RateLimitModal() {
// Close the modal so user can see the terminal
hideRateLimitModal();

// Alert the user about the terminal
alert(
`A terminal has been opened to authenticate "${profileName}".\n\n` +
`Steps to complete:\n` +
`1. Check the "Agent Terminals" section in the sidebar\n` +
`2. Complete the OAuth login in your browser\n` +
`3. The token will be saved automatically\n\n` +
`Once done, return here and the account will be available.`
);
// Notify the user about the terminal (non-blocking)
toast({
title: t('rateLimit.toast.authenticating', { profileName }),
description: t('rateLimit.toast.checkTerminal'),
});
} else {
alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`);
toast({
variant: 'destructive',
title: t('rateLimit.toast.authStartFailed'),
description: initResult.error || t('rateLimit.toast.tryAgain'),
});
}
}
} catch (err) {
console.error('Failed to add profile:', err);
alert('Failed to add profile. Please try again.');
debugError('[RateLimitModal] Failed to add profile:', err);
toast({
variant: 'destructive',
title: t('rateLimit.toast.addProfileFailed'),
description: t('rateLimit.toast.tryAgain'),
});
} finally {
setIsAddingProfile(false);
}
Expand Down
Loading
Loading