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
6 changes: 6 additions & 0 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ interface BaseChatProps {
enhancingPrompt?: boolean;
promptEnhanced?: boolean;
input?: string;
planMode?: boolean;
setPlanMode?: (enabled: boolean) => void;
model?: string;
setModel?: (model: string) => void;
provider?: ProviderInfo;
Expand Down Expand Up @@ -97,6 +99,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setProvider,
providerList,
input = '',
planMode,
setPlanMode,
enhancingPrompt,
handleInputChange,

Expand Down Expand Up @@ -442,6 +446,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setImageDataList={setImageDataList}
textareaRef={textareaRef}
input={input}
planMode={planMode}
setPlanMode={setPlanMode}
handleInputChange={handleInputChange}
handlePaste={handlePaste}
TEXTAREA_MIN_HEIGHT={TEXTAREA_MIN_HEIGHT}
Expand Down
37 changes: 36 additions & 1 deletion app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useShortcuts } from '~/lib/hooks';
import { description, useChatHistory } from '~/lib/persistence';
import { chatId } from '~/lib/persistence/useChatHistory';
import { getProjectPlanMode, setProjectPlanMode } from '~/lib/persistence/projectPlanMode';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
Expand Down Expand Up @@ -101,6 +103,9 @@ export const ChatImpl = memo(
);
const supabaseAlert = useStore(workbenchStore.supabaseAlert);
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
const currentChatId = useStore(chatId);
const [planMode, setPlanMode] = useState(false);
const skipNextPlanModeSave = useRef(false);
const [llmErrorAlert, setLlmErrorAlert] = useState<LlmErrorAlertType | undefined>(undefined);
const [model, setModel] = useState(() => {
const savedModel = Cookies.get('selectedModel');
Expand All @@ -117,6 +122,33 @@ export const ChatImpl = memo(
const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(null);
const mcpSettings = useMCPStore((state) => state.settings);

useEffect(() => {
if (!currentChatId) {
setPlanMode(false);
skipNextPlanModeSave.current = false;

return;
}

skipNextPlanModeSave.current = true;

const settings = getProjectPlanMode(currentChatId);
setPlanMode(settings.enabled);
}, [currentChatId]);

useEffect(() => {
if (!currentChatId) {
return;
}

if (skipNextPlanModeSave.current) {
skipNextPlanModeSave.current = false;
return;
}

setProjectPlanMode(currentChatId, { enabled: planMode });
}, [currentChatId, planMode]);

const {
messages,
isLoading,
Expand All @@ -140,6 +172,7 @@ export const ChatImpl = memo(
contextOptimization: contextOptimizationEnabled,
chatMode,
designScheme,
planMode,
supabase: {
isConnected: supabaseConn.isConnected,
hasSelectedProject: !!selectedProject,
Expand Down Expand Up @@ -344,7 +377,7 @@ export const ChatImpl = memo(
];

// Add image parts if any
images.forEach((imageData) => {
images.filter(Boolean).forEach((imageData) => {
// Extract correct MIME type from the data URL
const mimeType = imageData.split(';')[0].split(':')[1] || 'image/jpeg';

Expand Down Expand Up @@ -643,6 +676,8 @@ export const ChatImpl = memo(
apiKeys,
);
}}
planMode={planMode}
setPlanMode={setPlanMode}
uploadedFiles={uploadedFiles}
setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList}
Expand Down
15 changes: 15 additions & 0 deletions app/components/chat/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ interface ChatBoxProps {
handleStop?: (() => void) | undefined;
enhancingPrompt?: boolean | undefined;
enhancePrompt?: (() => void) | undefined;
planMode?: boolean;
setPlanMode?: ((enabled: boolean) => void) | undefined;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
designScheme?: DesignScheme;
Expand Down Expand Up @@ -262,6 +264,19 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
<div className="flex gap-1 items-center">
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
<McpTools />
<IconButton
title="Plan mode"
className={classNames(
'transition-all flex items-center gap-1 px-1.5',
props.planMode
? 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent'
: 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault',
)}
onClick={() => props.setPlanMode?.(!props.planMode)}
>
<div className="i-ph:list-checks text-xl" />
{props.planMode ? <span>Plan</span> : <span />}
</IconButton>
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
Expand Down
12 changes: 12 additions & 0 deletions app/lib/.server/llm/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export async function streamText(props: {
files?: FileMap;
providerSettings?: Record<string, IProviderSetting>;
promptId?: string;
planMode?: boolean;
contextOptimization?: boolean;
contextFiles?: FileMap;
summary?: string;
Expand All @@ -74,6 +75,7 @@ export async function streamText(props: {
files,
providerSettings,
promptId,
planMode,
contextOptimization,
contextFiles,
summary,
Expand Down Expand Up @@ -219,6 +221,16 @@ export async function streamText(props: {
console.log('No locked files found from any source for prompt.');
}

if (planMode) {
systemPrompt = `${systemPrompt}

PLANNING MODE:
- Before making code changes, create or update a PLAN.md file with a concise checklist of steps.
- Keep the plan scoped and actionable, and update it as steps are completed.
- After PLAN.md exists, proceed with the requested changes.
`;
}

logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);

// Log reasoning model detection and token parameters
Expand Down
2 changes: 2 additions & 0 deletions app/lib/persistence/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import type { Message } from 'ai';
import type { IChatMetadata } from './db'; // Import IChatMetadata
import { clearProjectPlanMode } from './projectPlanMode';

export interface ChatMessage {
id: string;
Expand Down Expand Up @@ -109,6 +110,7 @@ export async function deleteChat(db: IDBDatabase, id: string): Promise<void> {
const request = store.delete(id);

request.onsuccess = () => {
clearProjectPlanMode(id);
resolve();
};

Expand Down
2 changes: 2 additions & 0 deletions app/lib/persistence/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Message } from 'ai';
import { clearProjectPlanMode } from './projectPlanMode';
import { createScopedLogger } from '~/utils/logger';
import type { ChatHistoryItem } from './useChatHistory';
import type { Snapshot } from './types'; // Import Snapshot type
Expand Down Expand Up @@ -135,6 +136,7 @@ export async function deleteById(db: IDBDatabase, id: string): Promise<void> {

const checkCompletion = () => {
if (chatDeleted && snapshotDeleted) {
clearProjectPlanMode(id);
resolve(undefined);
}
};
Expand Down
78 changes: 78 additions & 0 deletions app/lib/persistence/projectPlanMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { getLocalStorage, setLocalStorage } from './localStorage';

const PROJECT_PLAN_MODE_PREFIX = 'bolt_project_plan_mode';
const isClient = typeof window !== 'undefined' && typeof localStorage !== 'undefined';

export interface ProjectPlanModeSettings {
enabled: boolean;
}

const defaultSettings: ProjectPlanModeSettings = {
enabled: false,
};

function buildKey(chatId?: string) {
return chatId ? `${PROJECT_PLAN_MODE_PREFIX}:${chatId}` : undefined;
}

export function getProjectPlanMode(chatId?: string): ProjectPlanModeSettings {
if (!chatId) {
return { ...defaultSettings };
}

const stored = getLocalStorage(buildKey(chatId) as string);

return {
...defaultSettings,
...(stored || {}),
};
}

export function setProjectPlanMode(chatId: string | undefined, updates: Partial<ProjectPlanModeSettings>): void {
if (!chatId) {
return;
}

const key = buildKey(chatId);

if (!key) {
return;
}

const current = getProjectPlanMode(chatId);
setLocalStorage(key, { ...current, ...updates });
}

export function clearProjectPlanMode(chatId: string | undefined): void {
if (!chatId || !isClient) {
return;
}

const key = buildKey(chatId);

if (!key) {
return;
}

try {
localStorage.removeItem(key);
} catch (error) {
console.error(`Error clearing plan mode settings for chat "${chatId}":`, error);
}
}

export function clearAllProjectPlanMode(): void {
if (!isClient) {
return;
}

try {
Object.keys(localStorage)
.filter((key) => key.startsWith(`${PROJECT_PLAN_MODE_PREFIX}:`))
.forEach((key) => {
localStorage.removeItem(key);
});
} catch (error) {
console.error('Error clearing plan mode settings:', error);
}
}
4 changes: 4 additions & 0 deletions app/lib/services/importExportService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Cookies from 'js-cookie';
import { type Message } from 'ai';
import { getAllChats, deleteChat } from '~/lib/persistence/chats';
import { clearAllProjectPlanMode } from '~/lib/persistence/projectPlanMode';

interface ExtendedMessage extends Message {
name?: string;
Expand Down Expand Up @@ -329,6 +330,8 @@ export class ImportExportService {
await Promise.all(deletePromises);
}

clearAllProjectPlanMode();

// 4. Clear any chat snapshots
const snapshotKeys = Object.keys(localStorage).filter((key) => key.startsWith('snapshot:'));
snapshotKeys.forEach((key) => {
Expand Down Expand Up @@ -357,6 +360,7 @@ export class ImportExportService {
const chats = await getAllChats(db);
const deletePromises = chats.map((chat) => deleteChat(db, chat.id));
await Promise.all(deletePromises);
clearAllProjectPlanMode();
}

// Private helper methods
Expand Down
5 changes: 4 additions & 1 deletion app/routes/api.chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
},
});

const { messages, files, promptId, contextOptimization, supabase, chatMode, designScheme, maxLLMSteps } =
const { messages, files, promptId, contextOptimization, supabase, chatMode, designScheme, maxLLMSteps, planMode } =
await request.json<{
messages: Messages;
files: any;
promptId?: string;
contextOptimization: boolean;
chatMode: 'discuss' | 'build';
designScheme?: DesignScheme;
planMode?: boolean;
supabase?: {
isConnected: boolean;
hasSelectedProject: boolean;
Expand Down Expand Up @@ -278,6 +279,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextFiles: filteredFiles,
chatMode,
designScheme,
planMode,
summary,
messageSliceId,
});
Expand Down Expand Up @@ -319,6 +321,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextFiles: filteredFiles,
chatMode,
designScheme,
planMode,
summary,
messageSliceId,
});
Expand Down
Loading