Skip to content
Closed
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,15 @@ Bolt.new supports most popular JavaScript frameworks and libraries. If it runs o

**How can I add make sure my framework/project works well in bolt?**
We are excited to work with the JavaScript ecosystem to improve functionality in Bolt. Reach out to us via [hello@stackblitz.com](mailto:hello@stackblitz.com) to discuss how we can partner!

## Codex Mode

Bolt includes an integration with [OpenAI Codex](https://openai.com/index/introducing-codex/) — a cloud-based software engineering agent powered by `codex-mini-latest`.

### Enabling Codex Mode
1. Add your OpenAI API key in **Settings → Providers → OpenAI**
2. Toggle **Codex mode** in the chat toolbar (robot icon)
3. Describe a complex task and hit send

Codex will reason through your codebase, plan changes, and apply them directly to your project — the same way the AI normally works, just powered by a more deliberate reasoning model. Best for: large refactors, adding authentication, database schema migrations, test suites.

108 changes: 105 additions & 3 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames';
import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client';
import { CodexModeToggle } from './CodexModeToggle';
import { CodexProgress } from './CodexProgress';
import { useStore } from '@nanostores/react';
import { codexModeEnabled, codexStatus, codexCurrentTask, codexElapsedSeconds } from '~/lib/stores/codex';
import { chatStore } from '~/lib/stores/chat';

import styles from './BaseChat.module.scss';

Expand Down Expand Up @@ -58,6 +63,93 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
ref,
) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const isCodexMode = useStore(codexModeEnabled);

React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key.toLowerCase() === 'c' && e.shiftKey && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
codexModeEnabled.set(!codexModeEnabled.get());
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

const handleCodexSubmit = async () => {
const currentInput = input;
if (!currentInput) return;
handleInputChange?.({ target: { value: '' } } as any);

const { workbenchStore } = await import('~/lib/stores/workbench');
const files = workbenchStore.files.get();
const serializedFiles = Object.fromEntries(
Object.entries(files)
.filter(([, f]) => f?.type === 'file')
.map(([p, f]) => [p, f?.content ?? ''])
);

codexStatus.set('running');
codexCurrentTask.set({
id: `codex-${Date.now()}`,
task: currentInput,
status: 'running',
startedAt: Date.now()
});
codexElapsedSeconds.set(0);

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120000);

try {
const response = await fetch('/api/codex', {
method: 'POST',
body: JSON.stringify({ task: currentInput, files: serializedFiles }),
headers: { 'Content-Type': 'application/json' },
signal: controller.signal
});

clearTimeout(timeout);

if (!response.ok) {
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
codexStatus.set('error');
codexCurrentTask.set({ ...codexCurrentTask.get()!, status: 'error', error: err.error });
return;
}

const reader = response.body?.getReader();
if (!reader) throw new Error('No stream available');

const decoder = new TextDecoder();
let accumulated = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;
accumulated += decoder.decode(value, { stream: true });
}

chatStore.setKey('messages', [
...chatStore.get().messages,
{ id: Date.now().toString(), role: 'user', content: currentInput },
{ id: (Date.now() + 1).toString(), role: 'assistant', content: accumulated }
]);

workbenchStore.applyCodexArtifact(accumulated);
codexStatus.set('complete');
codexCurrentTask.set({ ...codexCurrentTask.get()!, status: 'complete', completedAt: Date.now() });

} catch (error: any) {
clearTimeout(timeout);
codexStatus.set('error');
codexCurrentTask.set({
...codexCurrentTask.get()!,
status: 'error',
error: error.name === 'AbortError' ? 'Codex timed out after 2 minutes. Try a smaller task.' : error.message
});
}
};

return (
<div
Expand Down Expand Up @@ -108,6 +200,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
)}
>
<CodexProgress />
<textarea
ref={textareaRef}
className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
Expand All @@ -119,7 +212,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(

event.preventDefault();

sendMessage?.(event);
if (isCodexMode) {
handleCodexSubmit();
} else {
sendMessage?.(event);
}
}
}}
value={input}
Expand All @@ -130,7 +227,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
minHeight: TEXTAREA_MIN_HEIGHT,
maxHeight: TEXTAREA_MAX_HEIGHT,
}}
placeholder="How can Bolt help you today?"
placeholder={isCodexMode ? "Describe a task for Codex... (e.g. 'Add user authentication with JWT')" : "How can Bolt help you today?"}
translate="no"
/>
<ClientOnly>
Expand All @@ -144,13 +241,18 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
return;
}

sendMessage?.(event);
if (isCodexMode) {
handleCodexSubmit();
} else {
sendMessage?.(event);
}
}}
/>
)}
</ClientOnly>
<div className="flex justify-between text-sm p-4 pt-2">
<div className="flex gap-1 items-center">
<CodexModeToggle />
<IconButton
title="Enhance prompt"
disabled={input.length === 0 || enhancingPrompt}
Expand Down
32 changes: 32 additions & 0 deletions app/components/chat/CodexModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useStore } from '@nanostores/react';
import { classNames } from '~/utils/classNames';
import { codexModeEnabled } from '~/lib/stores/codex';

export function CodexModeToggle() {
const isEnabled = useStore(codexModeEnabled);

return (
<button
onClick={() => codexModeEnabled.set(!isEnabled)}
className={classNames(
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-all mr-2',
isEnabled
? 'ring-1 ring-green-500/50 bg-green-500/10 text-green-400'
: 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary'
)}
title="Codex mode uses OpenAI's reasoning model for complex tasks. Takes 30–120 seconds."
>
{isEnabled ? (
<>
<div className="i-ph:robot-fill" />
Codex
</>
) : (
<>
<div className="i-ph:lightning-fill" />
Instant
</>
)}
</button>
);
}
92 changes: 92 additions & 0 deletions app/components/chat/CodexProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect, useState } from 'react';
import { useStore } from '@nanostores/react';
import { codexStatus, codexCurrentTask, codexElapsedSeconds } from '~/lib/stores/codex';
import { classNames } from '~/utils/classNames';

const STATUS_MESSAGES = [
'Reasoning through your codebase...',
'Planning file changes...',
'Writing implementation...',
'Reviewing output...',
];

export function CodexProgress() {
const status = useStore(codexStatus);
const task = useStore(codexCurrentTask);
const elapsed = useStore(codexElapsedSeconds);
const [messageIndex, setMessageIndex] = useState(0);

useEffect(() => {
if (status !== 'running') return;

const interval = setInterval(() => {
setMessageIndex((prev) => (prev + 1) % STATUS_MESSAGES.length);
}, 8000);

return () => clearInterval(interval);
}, [status]);

if (status === 'idle') return <div className="invisible h-12" />;

const isError = status === 'error';
const isComplete = status === 'complete';

return (
<div className="absolute bottom-full left-0 w-full px-6 pb-2 z-10">
<div
className={classNames(
'flex items-center gap-3 px-4 py-3 mx-auto max-w-chat rounded-lg border shadow-sm bg-bolt-elements-background-depth-2 backdrop-blur-md',
isError ? 'border-red-500/50' : isComplete ? 'border-green-500/50' : 'border-bolt-elements-borderColor'
)}
>
<div className="flex-shrink-0">
{isError ? (
<div className="i-ph:warning-circle-fill text-red-500 text-xl" />
) : isComplete ? (
<div className="i-ph:check-circle-fill text-green-500 text-xl" />
) : (
<div className="i-ph:spinner-gap animate-spin text-bolt-elements-textPrimary text-xl" />
)}
</div>

<div className="flex-1 min-w-0 flex flex-col justify-center">
<div className="flex justify-between items-baseline gap-2">
<span
className={classNames(
'text-sm truncate',
isError ? 'text-red-500' : isComplete ? 'text-green-500' : 'text-bolt-elements-textPrimary'
)}
>
{task?.task || 'Processing task'}
</span>
<span className="text-xs text-bolt-elements-textTertiary flex-shrink-0">
{isError ? 'Failed' : isComplete ? `Done · ${elapsed}s` : `Running · ${elapsed}s`}
</span>
</div>

{status === 'running' && (
<div className="text-xs text-bolt-elements-textSecondary mt-0.5">
{STATUS_MESSAGES[messageIndex]}
</div>
)}

{isError && task?.error && (
<div className="text-xs text-red-400 mt-0.5 break-words">
{task.error}
</div>
)}
</div>

{isError && (
<button
onClick={() => window.dispatchEvent(new CustomEvent('codex:retry'))}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors"
>
<div className="i-ph:arrow-clockwise" />
Retry
</button>
)}
</div>
</div>
);
}
91 changes: 91 additions & 0 deletions app/components/settings/SettingsWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useState, useEffect } from 'react';
import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
import { classNames } from '~/utils/classNames';

export function SettingsWindow({ open, onClose }: { open: boolean; onClose: () => void }) {
const [openaiKey, setOpenaiKey] = useState('');
const [showKey, setShowKey] = useState(false);

useEffect(() => {
// Load from cookie
const cookies = document.cookie.split('; ').reduce((acc, curr) => {
const [key, val] = curr.split('=');
acc[key] = decodeURIComponent(val || '');
return acc;
}, {} as Record<string, string>);

if (cookies.openai_api_key) {
setOpenaiKey(cookies.openai_api_key);
}
}, [open]);

const saveKey = (val: string) => {
setOpenaiKey(val);
document.cookie = `openai_api_key=${encodeURIComponent(val)}; path=/; max-age=31536000`;
};

const isConnected = openaiKey.length > 0;

return (
<DialogRoot open={open}>
<Dialog onBackdrop={onClose} onClose={onClose}>
<DialogTitle>Settings</DialogTitle>
<DialogDescription asChild>
<div className="p-5 flex flex-col gap-6">

<div className="flex flex-col gap-3">
<h3 className="text-bolt-elements-textPrimary font-medium">Providers</h3>

<div className="flex flex-col gap-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1">
<div className="flex justify-between items-center">
<div className="font-medium text-bolt-elements-textPrimary">OpenAI</div>
{isConnected ? (
<span className="px-2 py-0.5 rounded-full bg-green-500/10 text-green-500 text-xs font-medium border border-green-500/20">
Connected
</span>
) : (
<span className="px-2 py-0.5 rounded-full bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary text-xs font-medium border border-bolt-elements-borderColor">
Not configured
</span>
)}
</div>

<div className="flex flex-col gap-1 mt-2">
<label className="text-xs text-bolt-elements-textSecondary">
OpenAI API Key (for Codex mode)
</label>
<div className="relative">
<input
type={showKey ? 'text' : 'password'}
value={openaiKey}
onChange={(e) => saveKey(e.target.value)}
className="w-full px-3 py-2 rounded-md border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary text-sm focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColor"
placeholder="sk-..."
/>
<button
type="button"
onClick={() => setShowKey(!showKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary"
>
{showKey ? <div className="i-ph:eye-slash-fill" /> : <div className="i-ph:eye-fill" />}
</button>
</div>
<div className="text-xs text-bolt-elements-textTertiary mt-1">
Required for Codex mode. Get your key at <a href="https://platform.openai.com" target="_blank" rel="noreferrer" className="text-bolt-elements-textSecondary hover:underline">platform.openai.com</a>
</div>
</div>
</div>

</div>

</div>
</DialogDescription>
<div className="px-5 pb-4 bg-bolt-elements-background-depth-2 flex gap-2 justify-end">
<DialogButton type="primary" onClick={onClose}>
Done
</DialogButton>
</div>
</Dialog>
</DialogRoot>
);
}
Loading
Loading