Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/claude/claudeLocalLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { claudeLocal } from "./claudeLocal";
import { Session } from "./session";
import { Future } from "@/utils/future";
import { createSessionScanner } from "./utils/sessionScanner";
import { formatErrorForUi } from "@/utils/formatErrorForUi";

export async function claudeLocalLauncher(session: Session): Promise<'switch' | 'exit'> {

Expand Down Expand Up @@ -123,7 +124,7 @@ export async function claudeLocalLauncher(session: Session): Promise<'switch' |
} catch (e) {
logger.debug('[local]: launch error', e);
if (!exitReason) {
session.client.sendSessionEvent({ type: 'message', message: 'Process exited unexpectedly' });
session.client.sendSessionEvent({ type: 'message', message: `Claude process error: ${formatErrorForUi(e)}` });
continue;
} else {
break;
Expand Down
3 changes: 2 additions & 1 deletion src/claude/claudeRemoteLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EnhancedMode } from "./loop";
import { RawJSONLines } from "@/claude/types";
import { OutgoingMessageQueue } from "./utils/OutgoingMessageQueue";
import { getToolName } from "./utils/getToolName";
import { formatErrorForUi } from "@/utils/formatErrorForUi";

interface PermissionsField {
date: number;
Expand Down Expand Up @@ -402,7 +403,7 @@ export async function claudeRemoteLauncher(session: Session): Promise<'switch' |
} catch (e) {
logger.debug('[remote]: launch error', e);
if (!exitReason) {
session.client.sendSessionEvent({ type: 'message', message: 'Process exited unexpectedly' });
session.client.sendSessionEvent({ type: 'message', message: `Claude process error: ${formatErrorForUi(e)}` });
continue;
}
} finally {
Expand Down
61 changes: 57 additions & 4 deletions src/codex/runCodex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import { startHappyServer } from '@/claude/utils/startHappyServer';
import { MessageBuffer } from "@/ui/ink/messageBuffer";
import { CodexDisplay } from "@/ui/ink/CodexDisplay";
import { trimIdent } from "@/utils/trimIdent";
import type { CodexSessionConfig } from './types';
import type { CodexSessionConfig, CodexToolResponse } from './types';
import { CHANGE_TITLE_INSTRUCTION } from '@/gemini/constants';
import { notifyDaemonSessionStarted } from "@/daemon/controlClient";
import { registerKillSessionHandler } from "@/claude/registerKillSessionHandler";
import { delay } from "@/utils/time";
import { stopCaffeinate } from "@/utils/caffeinate";
import { formatErrorForUi } from "@/utils/formatErrorForUi";

type ReadyEventOptions = {
pending: unknown;
Expand Down Expand Up @@ -391,12 +392,46 @@ export async function runCodex(opts: {
session.sendCodexMessage(message);
});
client.setPermissionHandler(permissionHandler);

function extractCodexToolErrorText(response: CodexToolResponse): string | null {
if (!response?.isError) {
return null;
}
const text = (response.content || [])
.map((c) => (c && typeof c.text === 'string' ? c.text : ''))
.filter(Boolean)
.join('\n')
.trim();
return text || 'Codex error';
}

function forwardCodexStatusToUi(messageText: string): void {
messageBuffer.addMessage(messageText, 'status');
session.sendSessionEvent({ type: 'message', message: messageText });
}

function forwardCodexErrorToUi(errorText: string): void {
forwardCodexStatusToUi(`Codex error: ${errorText}`);
}

client.setHandler((msg) => {
logger.debug(`[Codex] MCP message: ${JSON.stringify(msg)}`);

// Add messages to the ink UI buffer based on message type
if (msg.type === 'agent_message') {
messageBuffer.addMessage(msg.message, 'assistant');
} else if (msg.type === 'error') {
const errorText = typeof msg.message === 'string' && msg.message.trim() ? msg.message.trim() : 'Codex error';
forwardCodexErrorToUi(errorText);
} else if (msg.type === 'stream_error') {
const status = typeof msg.message === 'string' && msg.message.trim() ? msg.message.trim() : 'Codex stream error';
forwardCodexStatusToUi(`Codex stream error: ${status}`);
} else if (msg.type === 'mcp_startup_update') {
if (msg.status?.state === 'failed') {
const errorText = typeof msg.status?.error === 'string' ? msg.status.error : 'MCP server failed to start';
const serverName = typeof msg.server === 'string' ? msg.server : 'unknown';
forwardCodexStatusToUi(`MCP server "${serverName}" failed to start: ${errorText}`);
}
} else if (msg.type === 'agent_reasoning_delta') {
// Skip reasoning deltas in the UI to reduce noise
} else if (msg.type === 'agent_reasoning') {
Expand Down Expand Up @@ -671,10 +706,18 @@ export async function runCodex(opts: {
(startConfig.config as any).experimental_resume = resumeFile;
}

await client.startSession(
const startResponse = await client.startSession(
startConfig,
{ signal: abortController.signal }
);
const startError = extractCodexToolErrorText(startResponse);
if (startError) {
forwardCodexErrorToUi(startError);
client.clearSession();
wasCreated = false;
currentModeHash = null;
continue;
}
wasCreated = true;
first = false;
} else {
Expand All @@ -683,6 +726,14 @@ export async function runCodex(opts: {
{ signal: abortController.signal }
);
logger.debug('[Codex] continueSession response:', response);
const continueError = extractCodexToolErrorText(response);
if (continueError) {
forwardCodexErrorToUi(continueError);
client.clearSession();
wasCreated = false;
currentModeHash = null;
continue;
}
}
} catch (error) {
logger.warn('Error in codex session:', error);
Expand All @@ -697,8 +748,10 @@ export async function runCodex(opts: {
currentModeHash = null;
logger.debug('[Codex] Marked session as not created after abort for proper resume');
} else {
messageBuffer.addMessage('Process exited unexpectedly', 'status');
session.sendSessionEvent({ type: 'message', message: 'Process exited unexpectedly' });
const details = formatErrorForUi(error);
const messageText = `Codex process error: ${details}`;
messageBuffer.addMessage(messageText, 'status');
session.sendSessionEvent({ type: 'message', message: messageText });
// For unexpected exits, try to store session for potential recovery
if (client.hasActiveSession()) {
storedSessionIdForResume = client.storeSessionForResume();
Expand Down
3 changes: 2 additions & 1 deletion src/gemini/runGemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { MessageBuffer } from '@/ui/ink/messageBuffer';
import { notifyDaemonSessionStarted } from '@/daemon/controlClient';
import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler';
import { stopCaffeinate } from '@/utils/caffeinate';
import { formatErrorForUi } from '@/utils/formatErrorForUi';

import { createGeminiBackend } from '@/agent/acp/gemini';
import type { AgentBackend, AgentMessage } from '@/agent/AgentBackend';
Expand Down Expand Up @@ -1039,7 +1040,7 @@ export async function runGemini(opts: {
errorMsg = errorDetails || errorMessage || errObj.message;
}
} else if (error instanceof Error) {
errorMsg = error.message;
errorMsg = formatErrorForUi(error);
}

messageBuffer.addMessage(errorMsg, 'status');
Expand Down
14 changes: 14 additions & 0 deletions src/utils/formatErrorForUi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Convert an unknown thrown value into a user-visible string.
*
* Intended for UI surfaces (TUI/mobile) where giant stacks can be noisy; we keep a generous cap.
*/
export function formatErrorForUi(error: unknown, opts?: { maxChars?: number }): string {
const maxChars = Math.max(1000, opts?.maxChars ?? 50_000);
const msg = error instanceof Error
? (error.stack || error.message || String(error))
: String(error);

return msg.length > maxChars ? `${msg.slice(0, maxChars)}\n…[truncated]` : msg;
}