refactor: improve error handling and CLI integration

- Updated CodexProvider to read prompts from stdin to prevent shell escaping issues.
- Enhanced AgentService to handle streamed error messages from providers, ensuring a consistent user experience.
- Modified UI components to display error messages clearly, including visual indicators for errors in chat bubbles.
- Updated CLI status handling to support both Claude and Codex APIs, improving compatibility and user feedback.

These changes enhance the robustness of the application and improve the user experience during error scenarios.
This commit is contained in:
DhanushSantosh
2026-01-06 14:10:48 +05:30
parent a57dcc170d
commit 27c6d5a3bb
13 changed files with 145 additions and 38 deletions

View File

@@ -765,7 +765,7 @@ export class CodexProvider extends BaseProvider {
...(outputSchemaPath ? [CODEX_OUTPUT_SCHEMA_FLAG, outputSchemaPath] : []),
...(imagePaths.length > 0 ? [CODEX_IMAGE_FLAG, imagePaths.join(',')] : []),
...configOverrides,
promptText,
'-', // Read prompt from stdin to avoid shell escaping issues
];
const stream = spawnJSONLProcess({
@@ -775,6 +775,7 @@ export class CodexProvider extends BaseProvider {
env: buildEnv(),
abortController: options.abortController,
timeout: DEFAULT_TIMEOUT_MS,
stdinData: promptText, // Pass prompt via stdin
});
for await (const rawEvent of stream) {

View File

@@ -13,6 +13,8 @@ import {
isAbortError,
loadContextFiles,
createLogger,
classifyError,
getUserFriendlyErrorMessage,
} from '@automaker/utils';
import { ProviderFactory } from '../providers/provider-factory.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
@@ -374,6 +376,53 @@ export class AgentService {
content: responseText,
toolUses,
});
} else if (msg.type === 'error') {
// Some providers (like Codex CLI/SaaS or Cursor CLI) surface failures as
// streamed error messages instead of throwing. Handle these here so the
// Agent Runner UX matches the Claude/Cursor behavior without changing
// their provider implementations.
const rawErrorText =
(typeof msg.error === 'string' && msg.error.trim()) ||
'Unexpected error from provider during agent execution.';
const errorInfo = classifyError(new Error(rawErrorText));
// Keep the provider-supplied text intact (Codex already includes helpful tips),
// only add a small rate-limit hint when we can detect it.
const enhancedText = errorInfo.isRateLimit
? `${rawErrorText}\n\nTip: It looks like you hit a rate limit. Try waiting a bit or reducing concurrent Agent Runner / Auto Mode tasks.`
: rawErrorText;
this.logger.error('Provider error during agent execution:', {
type: errorInfo.type,
message: errorInfo.message,
});
// Mark session as no longer running so the UI and queue stay in sync
session.isRunning = false;
session.abortController = null;
const errorMessage: Message = {
id: this.generateId(),
role: 'assistant',
content: `Error: ${enhancedText}`,
timestamp: new Date().toISOString(),
isError: true,
};
session.messages.push(errorMessage);
await this.saveSession(sessionId, session.messages);
this.emitAgentEvent(sessionId, {
type: 'error',
error: enhancedText,
message: errorMessage,
});
// Don't continue streaming after an error message
return {
success: false,
};
}
}

View File

@@ -161,7 +161,6 @@ export function AgentView() {
isConnected={isConnected}
isProcessing={isProcessing}
currentTool={currentTool}
agentError={agentError}
messagesCount={messages.length}
showSessionManager={showSessionManager}
onToggleSessionManager={() => setShowSessionManager(!showSessionManager)}

View File

@@ -7,7 +7,6 @@ interface AgentHeaderProps {
isConnected: boolean;
isProcessing: boolean;
currentTool: string | null;
agentError: string | null;
messagesCount: number;
showSessionManager: boolean;
onToggleSessionManager: () => void;
@@ -20,7 +19,6 @@ export function AgentHeader({
isConnected,
isProcessing,
currentTool,
agentError,
messagesCount,
showSessionManager,
onToggleSessionManager,
@@ -61,7 +59,6 @@ export function AgentHeader({
<span className="font-medium">{currentTool}</span>
</div>
)}
{agentError && <span className="text-xs text-destructive font-medium">{agentError}</span>}
{currentSessionId && messagesCount > 0 && (
<Button
variant="ghost"

View File

@@ -7,6 +7,7 @@ interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: string;
isError?: boolean;
images?: ImageAttachment[];
}

View File

@@ -1,4 +1,4 @@
import { Bot, User, ImageIcon } from 'lucide-react';
import { Bot, User, ImageIcon, AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Markdown } from '@/components/ui/markdown';
import type { ImageAttachment } from '@/store/app-store';
@@ -9,6 +9,7 @@ interface Message {
content: string;
timestamp: string;
images?: ImageAttachment[];
isError?: boolean;
}
interface MessageBubbleProps {
@@ -16,6 +17,8 @@ interface MessageBubbleProps {
}
export function MessageBubble({ message }: MessageBubbleProps) {
const isError = message.isError && message.role === 'assistant';
return (
<div
className={cn(
@@ -27,12 +30,16 @@ export function MessageBubble({ message }: MessageBubbleProps) {
<div
className={cn(
'w-9 h-9 rounded-xl flex items-center justify-center shrink-0 shadow-sm',
message.role === 'assistant'
? 'bg-primary/10 ring-1 ring-primary/20'
: 'bg-muted ring-1 ring-border'
isError
? 'bg-red-500/10 ring-1 ring-red-500/20'
: message.role === 'assistant'
? 'bg-primary/10 ring-1 ring-primary/20'
: 'bg-muted ring-1 ring-border'
)}
>
{message.role === 'assistant' ? (
{isError ? (
<AlertCircle className="w-4 h-4 text-red-500" />
) : message.role === 'assistant' ? (
<Bot className="w-4 h-4 text-primary" />
) : (
<User className="w-4 h-4 text-muted-foreground" />
@@ -43,13 +50,22 @@ export function MessageBubble({ message }: MessageBubbleProps) {
<div
className={cn(
'flex-1 max-w-[85%] rounded-2xl px-4 py-3 shadow-sm',
message.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-card border border-border'
isError
? 'bg-red-500/10 border border-red-500/30'
: message.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-card border border-border'
)}
>
{message.role === 'assistant' ? (
<Markdown className="text-sm text-foreground prose-p:leading-relaxed prose-headings:text-foreground prose-strong:text-foreground prose-code:text-primary prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded">
<Markdown
className={cn(
'text-sm prose-p:leading-relaxed prose-headings:text-foreground prose-strong:text-foreground prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded',
isError
? 'text-red-600 dark:text-red-400 prose-code:text-red-600 dark:prose-code:text-red-400 prose-code:bg-red-500/10'
: 'text-foreground prose-code:text-primary prose-code:bg-muted'
)}
>
{message.content}
</Markdown>
) : (
@@ -95,7 +111,11 @@ export function MessageBubble({ message }: MessageBubbleProps) {
<p
className={cn(
'text-[11px] mt-2 font-medium',
message.role === 'user' ? 'text-primary-foreground/70' : 'text-muted-foreground'
isError
? 'text-red-500/70'
: message.role === 'user'
? 'text-primary-foreground/70'
: 'text-muted-foreground'
)}
>
{new Date(message.timestamp).toLocaleTimeString([], {

View File

@@ -8,7 +8,6 @@ import {
} from '@/lib/agent-context-parser';
import { cn } from '@/lib/utils';
import {
Cpu,
Brain,
ListTodo,
Sparkles,
@@ -20,6 +19,7 @@ import {
} from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { SummaryDialog } from './summary-dialog';
import { getProviderIconForModel } from '@/components/ui/provider-icon';
/**
* Formats thinking level for compact display
@@ -109,7 +109,10 @@ export function AgentInfoPanel({
<div className="mb-3 space-y-2 overflow-hidden">
<div className="flex items-center gap-2 text-[11px] flex-wrap">
<div className="flex items-center gap-1 text-[var(--status-info)]">
<Cpu className="w-3 h-3" />
{(() => {
const ProviderIcon = getProviderIconForModel(feature.model);
return <ProviderIcon className="w-3 h-3" />;
})()}
<span className="font-medium">{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
{feature.thinkingLevel && feature.thinkingLevel !== 'none' ? (
@@ -133,7 +136,10 @@ export function AgentInfoPanel({
{/* Model & Phase */}
<div className="flex items-center gap-2 text-[11px] flex-wrap">
<div className="flex items-center gap-1 text-[var(--status-info)]">
<Cpu className="w-3 h-3" />
{(() => {
const ProviderIcon = getProviderIconForModel(feature.model);
return <ProviderIcon className="w-3 h-3" />;
})()}
<span className="font-medium">{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
{agentInfo.currentPhase && (

View File

@@ -18,12 +18,12 @@ import {
MoreVertical,
ChevronDown,
ChevronUp,
Cpu,
GitFork,
} from 'lucide-react';
import { CountUpTimer } from '@/components/ui/count-up-timer';
import { formatModelName, DEFAULT_MODEL } from '@/lib/agent-context-parser';
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
import { getProviderIconForModel } from '@/components/ui/provider-icon';
interface CardHeaderProps {
feature: Feature;
@@ -109,12 +109,17 @@ export function CardHeaderSection({
Spawn Sub-Task
</DropdownMenuItem>
{/* Model info in dropdown */}
<div className="px-2 py-1.5 text-[10px] text-muted-foreground border-t mt-1 pt-1.5">
<div className="flex items-center gap-1">
<Cpu className="w-3 h-3" />
<span>{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
</div>
{(() => {
const ProviderIcon = getProviderIconForModel(feature.model);
return (
<div className="px-2 py-1.5 text-[10px] text-muted-foreground border-t mt-1 pt-1.5">
<div className="flex items-center gap-1">
<ProviderIcon className="w-3 h-3" />
<span>{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
</div>
);
})()}
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -288,12 +293,17 @@ export function CardHeaderSection({
Spawn Sub-Task
</DropdownMenuItem>
{/* Model info in dropdown */}
<div className="px-2 py-1.5 text-[10px] text-muted-foreground border-t mt-1 pt-1.5">
<div className="flex items-center gap-1">
<Cpu className="w-3 h-3" />
<span>{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
</div>
{(() => {
const ProviderIcon = getProviderIconForModel(feature.model);
return (
<div className="px-2 py-1.5 text-[10px] text-muted-foreground border-t mt-1 pt-1.5">
<div className="flex items-center gap-1">
<ProviderIcon className="w-3 h-3" />
<span>{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
</div>
);
})()}
</DropdownMenuContent>
</DropdownMenu>
</div>

View File

@@ -40,8 +40,13 @@ export function useCliStatus({
logger.info(`Raw status result for ${cliType}:`, result);
if (result.success) {
// Handle both response formats:
// - Claude API returns {status: 'installed' | 'not_installed'}
// - Codex API returns {installed: boolean}
const isInstalled =
typeof result.installed === 'boolean' ? result.installed : result.status === 'installed';
const cliStatus = {
installed: result.status === 'installed',
installed: isInstalled,
path: result.path || null,
version: result.version || null,
method: result.method || 'none',

View File

@@ -15,7 +15,6 @@ import { getElectronAPI } from '@/lib/electron';
import {
CheckCircle2,
Loader2,
Terminal,
Key,
ArrowRight,
ArrowLeft,
@@ -31,6 +30,7 @@ import {
import { toast } from 'sonner';
import { StatusBadge, TerminalOutput } from '../components';
import { useCliStatus, useCliInstallation, useTokenSave } from '../hooks';
import { AnthropicIcon } from '@/components/ui/provider-icon';
interface ClaudeSetupStepProps {
onNext: () => void;
@@ -310,7 +310,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
<div className="space-y-6">
<div className="text-center mb-8">
<div className="w-16 h-16 rounded-xl bg-brand-500/10 flex items-center justify-center mx-auto mb-4">
<Terminal className="w-8 h-8 text-brand-500" />
<AnthropicIcon className="w-8 h-8 text-brand-500" />
</div>
<h2 className="text-2xl font-bold text-foreground mb-2">Claude Code Setup</h2>
<p className="text-muted-foreground">Configure for code generation</p>
@@ -339,7 +339,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center justify-between w-full pr-4">
<div className="flex items-center gap-3">
<Terminal
<AnthropicIcon
className={`w-5 h-5 ${
cliVerificationStatus === 'verified'
? 'text-green-500'

View File

@@ -14,11 +14,11 @@ import {
Copy,
RefreshCw,
AlertTriangle,
Terminal,
XCircle,
} from 'lucide-react';
import { toast } from 'sonner';
import { StatusBadge } from '../components';
import { CursorIcon } from '@/components/ui/provider-icon';
const logger = createLogger('CursorSetupStep');
@@ -168,7 +168,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps
<div className="space-y-6">
<div className="text-center mb-8">
<div className="w-16 h-16 rounded-xl bg-cyan-500/10 flex items-center justify-center mx-auto mb-4">
<Terminal className="w-8 h-8 text-cyan-500" />
<CursorIcon className="w-8 h-8 text-cyan-500" />
</div>
<h2 className="text-2xl font-bold text-foreground mb-2">Cursor CLI Setup</h2>
<p className="text-muted-foreground">Optional - Use Cursor as an AI provider</p>
@@ -195,7 +195,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Terminal className="w-5 h-5" />
<CursorIcon className="w-5 h-5" />
Cursor CLI Status
<Badge variant="outline" className="ml-2">
Optional

View File

@@ -329,6 +329,17 @@ export function useElectronAgent({
if (event.message) {
const errorMessage = event.message;
setMessages((prev) => [...prev, errorMessage]);
} else {
// Some providers stream an error without a message payload. Ensure the
// user still sees a clear error bubble in the chat.
const fallbackMessage: Message = {
id: `err_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
role: 'assistant',
content: `Error: ${event.error}`,
timestamp: new Date().toISOString(),
isError: true,
};
setMessages((prev) => [...prev, fallbackMessage]);
}
break;

View File

@@ -44,11 +44,15 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
console.log(`[SubprocessManager] Passing ${stdinData.length} bytes via stdin`);
}
// On Windows, .cmd files must be run through shell (cmd.exe)
const needsShell = process.platform === 'win32' && command.toLowerCase().endsWith('.cmd');
const childProcess: ChildProcess = spawn(command, args, {
cwd,
env: processEnv,
// Use 'pipe' for stdin when we need to write data, otherwise 'ignore'
stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'],
shell: needsShell,
});
// Write stdin data if provided
@@ -194,10 +198,14 @@ export async function spawnProcess(options: SubprocessOptions): Promise<Subproce
};
return new Promise((resolve, reject) => {
// On Windows, .cmd files must be run through shell (cmd.exe)
const needsShell = process.platform === 'win32' && command.toLowerCase().endsWith('.cmd');
const childProcess = spawn(command, args, {
cwd,
env: processEnv,
stdio: ['ignore', 'pipe', 'pipe'],
shell: needsShell,
});
let stdout = '';