From 3c28e8ad8832b39a7062a6b997c08745f94d6c04 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Sat, 1 Mar 2025 01:34:35 +0530 Subject: [PATCH] fix: fix enhance prompt to stop implementing full project instead of enhancing (#1383) #release * fix: enhance prompt fix * fix: added error capture on api error * fix: replaced error with log for wrong files selected by bolt --- app/components/ui/Slider.tsx | 14 +- app/components/workbench/DiffView.tsx | 510 ++++++------ app/components/workbench/FileTree.tsx | 47 +- app/components/workbench/Workbench.client.tsx | 782 +++++++++--------- app/lib/.server/llm/select-context.ts | 12 +- app/lib/runtime/action-runner.ts | 8 +- app/routes/api.enhancer.ts | 27 + app/types/actions.ts | 1 + app/utils/getLanguageFromExtension.ts | 42 +- 9 files changed, 758 insertions(+), 685 deletions(-) diff --git a/app/components/ui/Slider.tsx b/app/components/ui/Slider.tsx index d5e9b000..70ec9539 100644 --- a/app/components/ui/Slider.tsx +++ b/app/components/ui/Slider.tsx @@ -4,11 +4,6 @@ import { classNames } from '~/utils/classNames'; import { cubicEasingFn } from '~/utils/easings'; import { genericMemo } from '~/utils/react'; -interface SliderOption { - value: T; - text: string; -} - export type SliderOptions = { left: { value: T; text: string }; middle?: { value: T; text: string }; @@ -31,14 +26,17 @@ export const Slider = genericMemo(({ selected, options, setSelected }: Slide setSelected?.(options.left.value)}> {options.left.text} - + {options.middle && ( setSelected?.(options.middle!.value)}> {options.middle.text} )} - - setSelected?.(options.right.value)}> + + setSelected?.(options.right.value)} + > {options.right.text} diff --git a/app/components/workbench/DiffView.tsx b/app/components/workbench/DiffView.tsx index e1cb843d..aa635ba6 100644 --- a/app/components/workbench/DiffView.tsx +++ b/app/components/workbench/DiffView.tsx @@ -41,15 +41,17 @@ const FullscreenButton = memo(({ onClick, isFullscreen }: FullscreenButtonProps) )); const FullscreenOverlay = memo(({ isFullscreen, children }: { isFullscreen: boolean; children: React.ReactNode }) => { - if (!isFullscreen) return <>{children}; - + if (!isFullscreen) { + return <>{children}; + } + return (
@@ -75,7 +77,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { hasChanges: false, lineChanges: { before: new Set(), after: new Set() }, unifiedBlocks: [], - isBinary: true + isBinary: true, }; } @@ -84,7 +86,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { return content .replace(/\r\n/g, '\n') .split('\n') - .map(line => line.trimEnd()); + .map((line) => line.trimEnd()); }; const beforeLines = normalizeContent(beforeCode); @@ -98,19 +100,21 @@ const processChanges = (beforeCode: string, afterCode: string) => { hasChanges: false, lineChanges: { before: new Set(), after: new Set() }, unifiedBlocks: [], - isBinary: false + isBinary: false, }; } const lineChanges = { before: new Set(), - after: new Set() + after: new Set(), }; const unifiedBlocks: DiffBlock[] = []; // Compare lines directly for more accurate diff - let i = 0, j = 0; + let i = 0, + j = 0; + while (i < beforeLines.length || j < afterLines.length) { if (i < beforeLines.length && j < afterLines.length && beforeLines[i] === afterLines[j]) { // Unchanged line @@ -118,7 +122,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { lineNumber: j, content: afterLines[j], type: 'unchanged', - correspondingLine: i + correspondingLine: i, }); i++; j++; @@ -138,7 +142,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { content: beforeLines[i + l], type: 'removed', correspondingLine: j, - charChanges: [{ value: beforeLines[i + l], type: 'removed' }] + charChanges: [{ value: beforeLines[i + l], type: 'removed' }], }); } i += k; @@ -153,7 +157,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { content: afterLines[j + l], type: 'added', correspondingLine: i, - charChanges: [{ value: afterLines[j + l], type: 'added' }] + charChanges: [{ value: afterLines[j + l], type: 'added' }], }); } j += k; @@ -167,20 +171,25 @@ const processChanges = (beforeCode: string, afterCode: string) => { if (i < beforeLines.length && j < afterLines.length) { const beforeLine = beforeLines[i]; const afterLine = afterLines[j]; - + // Find common prefix and suffix let prefixLength = 0; - while (prefixLength < beforeLine.length && - prefixLength < afterLine.length && - beforeLine[prefixLength] === afterLine[prefixLength]) { + + while ( + prefixLength < beforeLine.length && + prefixLength < afterLine.length && + beforeLine[prefixLength] === afterLine[prefixLength] + ) { prefixLength++; } - + let suffixLength = 0; - while (suffixLength < beforeLine.length - prefixLength && - suffixLength < afterLine.length - prefixLength && - beforeLine[beforeLine.length - 1 - suffixLength] === - afterLine[afterLine.length - 1 - suffixLength]) { + + while ( + suffixLength < beforeLine.length - prefixLength && + suffixLength < afterLine.length - prefixLength && + beforeLine[beforeLine.length - 1 - suffixLength] === afterLine[afterLine.length - 1 - suffixLength] + ) { suffixLength++; } @@ -201,11 +210,12 @@ const processChanges = (beforeCode: string, afterCode: string) => { charChanges: [ { value: prefix, type: 'unchanged' }, { value: beforeMiddle, type: 'removed' }, - { value: suffix, type: 'unchanged' } - ] + { value: suffix, type: 'unchanged' }, + ], }); i++; } + if (afterMiddle) { lineChanges.after.add(j); unifiedBlocks.push({ @@ -216,8 +226,8 @@ const processChanges = (beforeCode: string, afterCode: string) => { charChanges: [ { value: prefix, type: 'unchanged' }, { value: afterMiddle, type: 'added' }, - { value: suffix, type: 'unchanged' } - ] + { value: suffix, type: 'unchanged' }, + ], }); j++; } @@ -230,10 +240,11 @@ const processChanges = (beforeCode: string, afterCode: string) => { content: beforeLines[i], type: 'removed', correspondingLine: j, - charChanges: [{ value: beforeLines[i], type: 'removed' }] + charChanges: [{ value: beforeLines[i], type: 'removed' }], }); i++; } + if (j < afterLines.length) { lineChanges.after.add(j); unifiedBlocks.push({ @@ -241,7 +252,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { content: afterLines[j], type: 'added', correspondingLine: i - 1, - charChanges: [{ value: afterLines[j], type: 'added' }] + charChanges: [{ value: afterLines[j], type: 'added' }], }); j++; } @@ -255,10 +266,11 @@ const processChanges = (beforeCode: string, afterCode: string) => { content: beforeLines[i], type: 'removed', correspondingLine: j, - charChanges: [{ value: beforeLines[i], type: 'removed' }] + charChanges: [{ value: beforeLines[i], type: 'removed' }], }); i++; } + if (j < afterLines.length) { lineChanges.after.add(j); unifiedBlocks.push({ @@ -266,7 +278,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { content: afterLines[j], type: 'added', correspondingLine: i - 1, - charChanges: [{ value: afterLines[j], type: 'added' }] + charChanges: [{ value: afterLines[j], type: 'added' }], }); j++; } @@ -284,7 +296,7 @@ const processChanges = (beforeCode: string, afterCode: string) => { hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0, lineChanges, unifiedBlocks: processedBlocks, - isBinary: false + isBinary: false, }; } catch (error) { console.error('Error processing changes:', error); @@ -295,26 +307,28 @@ const processChanges = (beforeCode: string, afterCode: string) => { lineChanges: { before: new Set(), after: new Set() }, unifiedBlocks: [], error: true, - isBinary: false + isBinary: false, }; } }; -const lineNumberStyles = "w-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1"; -const lineContentStyles = "px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary"; -const diffPanelStyles = "h-full overflow-auto diff-panel-content"; +const lineNumberStyles = + 'w-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1'; +const lineContentStyles = + 'px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary'; +const diffPanelStyles = 'h-full overflow-auto diff-panel-content'; // Updated color styles for better consistency const diffLineStyles = { added: 'bg-green-500/10 dark:bg-green-500/20 border-l-4 border-green-500', removed: 'bg-red-500/10 dark:bg-red-500/20 border-l-4 border-red-500', - unchanged: '' + unchanged: '', }; const changeColorStyles = { added: 'text-green-700 dark:text-green-500 bg-green-500/10 dark:bg-green-500/20', removed: 'text-red-700 dark:text-red-500 bg-red-500/10 dark:bg-red-500/20', - unchanged: 'text-bolt-elements-textPrimary' + unchanged: 'text-bolt-elements-textPrimary', }; const renderContentWarning = (type: 'binary' | 'error') => ( @@ -325,50 +339,61 @@ const renderContentWarning = (type: 'binary' | 'error') => ( {type === 'binary' ? 'Binary file detected' : 'Error processing file'}

- {type === 'binary' - ? 'Diff view is not available for binary files' - : 'Could not generate diff preview'} + {type === 'binary' ? 'Diff view is not available for binary files' : 'Could not generate diff preview'}

); -const NoChangesView = memo(({ beforeCode, language, highlighter, theme }: { - beforeCode: string; - language: string; - highlighter: any; - theme: string; -}) => ( -
-
-
-

Files are identical

-

Both versions match exactly

-
-
-
- Current Content +const NoChangesView = memo( + ({ + beforeCode, + language, + highlighter, + theme, + }: { + beforeCode: string; + language: string; + highlighter: any; + theme: string; + }) => ( +
+
+
+

Files are identical

+

Both versions match exactly

-
- {beforeCode.split('\n').map((line, index) => ( -
-
{index + 1}
-
- - ]*>/g, '') - .replace(/<\/?code[^>]*>/g, '') - : line - }} /> +
+
+ Current Content +
+
+ {beforeCode.split('\n').map((line, index) => ( +
+
{index + 1}
+
+ + ]*>/g, '') + .replace(/<\/?code[^>]*>/g, '') + : line, + }} + /> +
-
- ))} + ))} +
-
-)); + ), +); // Otimização do processamento de diferenças com memoização const useProcessChanges = (beforeCode: string, afterCode: string) => { @@ -376,150 +401,154 @@ const useProcessChanges = (beforeCode: string, afterCode: string) => { }; // Componente otimizado para renderização de linhas de código -const CodeLine = memo(({ - lineNumber, - content, - type, - highlighter, - language, - block, - theme -}: { - lineNumber: number; - content: string; - type: 'added' | 'removed' | 'unchanged'; - highlighter: any; - language: string; - block: DiffBlock; - theme: string; -}) => { - const bgColor = diffLineStyles[type]; +const CodeLine = memo( + ({ + lineNumber, + content, + type, + highlighter, + language, + block, + theme, + }: { + lineNumber: number; + content: string; + type: 'added' | 'removed' | 'unchanged'; + highlighter: any; + language: string; + block: DiffBlock; + theme: string; + }) => { + const bgColor = diffLineStyles[type]; - const renderContent = () => { - if (type === 'unchanged' || !block.charChanges) { - const highlightedCode = highlighter ? - highlighter.codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' }) - .replace(/<\/?pre[^>]*>/g, '') - .replace(/<\/?code[^>]*>/g, '') - : content; - return ; - } + const renderContent = () => { + if (type === 'unchanged' || !block.charChanges) { + const highlightedCode = highlighter + ? highlighter + .codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' }) + .replace(/<\/?pre[^>]*>/g, '') + .replace(/<\/?code[^>]*>/g, '') + : content; + return ; + } + + return ( + <> + {block.charChanges.map((change, index) => { + const changeClass = changeColorStyles[change.type]; + + const highlightedCode = highlighter + ? highlighter + .codeToHtml(change.value, { + lang: language, + theme: theme === 'dark' ? 'github-dark' : 'github-light', + }) + .replace(/<\/?pre[^>]*>/g, '') + .replace(/<\/?code[^>]*>/g, '') + : change.value; + + return ; + })} + + ); + }; return ( - <> - {block.charChanges.map((change, index) => { - const changeClass = changeColorStyles[change.type]; - - const highlightedCode = highlighter ? - highlighter.codeToHtml(change.value, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' }) - .replace(/<\/?pre[^>]*>/g, '') - .replace(/<\/?code[^>]*>/g, '') - : change.value; - - return ( - - ); - })} - - ); - }; - - return ( -
-
{lineNumber + 1}
-
- - {type === 'added' && +} - {type === 'removed' && -} - {type === 'unchanged' && ' '} - - {renderContent()} +
+
{lineNumber + 1}
+
+ + {type === 'added' && +} + {type === 'removed' && -} + {type === 'unchanged' && ' '} + + {renderContent()} +
-
- ); -}); + ); + }, +); // Componente para exibir informações sobre o arquivo -const FileInfo = memo(({ - filename, - hasChanges, - onToggleFullscreen, - isFullscreen, - beforeCode, - afterCode -}: { - filename: string; - hasChanges: boolean; - onToggleFullscreen: () => void; - isFullscreen: boolean; - beforeCode: string; - afterCode: string; -}) => { - // Calculate additions and deletions from the current document - const { additions, deletions } = useMemo(() => { - if (!hasChanges) return { additions: 0, deletions: 0 }; - - const changes = diffLines(beforeCode, afterCode, { - newlineIsToken: false, - ignoreWhitespace: true, - ignoreCase: false - }); - - return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => { - if (change.added) { - acc.additions += change.value.split('\n').length; +const FileInfo = memo( + ({ + filename, + hasChanges, + onToggleFullscreen, + isFullscreen, + beforeCode, + afterCode, + }: { + filename: string; + hasChanges: boolean; + onToggleFullscreen: () => void; + isFullscreen: boolean; + beforeCode: string; + afterCode: string; + }) => { + // Calculate additions and deletions from the current document + const { additions, deletions } = useMemo(() => { + if (!hasChanges) { + return { additions: 0, deletions: 0 }; } - if (change.removed) { - acc.deletions += change.value.split('\n').length; - } - return acc; - }, { additions: 0, deletions: 0 }); - }, [hasChanges, beforeCode, afterCode]); - const showStats = additions > 0 || deletions > 0; + const changes = diffLines(beforeCode, afterCode, { + newlineIsToken: false, + ignoreWhitespace: true, + ignoreCase: false, + }); - return ( -
-
- {filename} - - {hasChanges ? ( - <> - {showStats && ( -
- {additions > 0 && ( - +{additions} - )} - {deletions > 0 && ( - -{deletions} - )} -
- )} - Modified - - {new Date().toLocaleTimeString()} - - - ) : ( - No Changes - )} - -
-
- ); -}); + return changes.reduce( + (acc: { additions: number; deletions: number }, change: Change) => { + if (change.added) { + acc.additions += change.value.split('\n').length; + } -const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, lightTheme, darkTheme }: CodeComparisonProps) => { + if (change.removed) { + acc.deletions += change.value.split('\n').length; + } + + return acc; + }, + { additions: 0, deletions: 0 }, + ); + }, [hasChanges, beforeCode, afterCode]); + + const showStats = additions > 0 || deletions > 0; + + return ( +
+
+ {filename} + + {hasChanges ? ( + <> + {showStats && ( +
+ {additions > 0 && +{additions}} + {deletions > 0 && -{deletions}} +
+ )} + Modified + {new Date().toLocaleTimeString()} + + ) : ( + No Changes + )} + +
+
+ ); + }, +); + +const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => { const [isFullscreen, setIsFullscreen] = useState(false); const [highlighter, setHighlighter] = useState(null); const theme = useStore(themeStore); - + const toggleFullscreen = useCallback(() => { - setIsFullscreen(prev => !prev); + setIsFullscreen((prev) => !prev); }, []); const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode); @@ -527,16 +556,18 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, useEffect(() => { getHighlighter({ themes: ['github-dark', 'github-light'], - langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'] + langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'], }).then(setHighlighter); }, []); - if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error'); + if (isBinary || error) { + return renderContentWarning(isBinary ? 'binary' : 'error'); + } return (
- ) : ( - + )}
@@ -580,7 +606,7 @@ interface DiffViewProps { actionRunner: ActionRunner; } -export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: DiffViewProps) => { +export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => { const files = useStore(workbenchStore.files) as FileMap; const selectedFile = useStore(workbenchStore.selectedFile); const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument; @@ -589,82 +615,80 @@ export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: Dif useEffect(() => { if (selectedFile && currentDocument) { const file = files[selectedFile]; - if (!file || !('content' in file)) return; + + if (!file || !('content' in file)) { + return; + } const existingHistory = fileHistory[selectedFile]; const currentContent = currentDocument.value; - + // Normalizar o conteúdo para comparação const normalizedCurrentContent = currentContent.replace(/\r\n/g, '\n').trim(); - const normalizedOriginalContent = (existingHistory?.originalContent || file.content).replace(/\r\n/g, '\n').trim(); - + const normalizedOriginalContent = (existingHistory?.originalContent || file.content) + .replace(/\r\n/g, '\n') + .trim(); + // Se não há histórico existente, criar um novo apenas se houver diferenças if (!existingHistory) { if (normalizedCurrentContent !== normalizedOriginalContent) { const newChanges = diffLines(file.content, currentContent); - setFileHistory(prev => ({ + setFileHistory((prev) => ({ ...prev, [selectedFile]: { originalContent: file.content, lastModified: Date.now(), changes: newChanges, - versions: [{ - timestamp: Date.now(), - content: currentContent - }], - changeSource: 'auto-save' - } + versions: [ + { + timestamp: Date.now(), + content: currentContent, + }, + ], + changeSource: 'auto-save', + }, })); } + return; } // Se já existe histórico, verificar se há mudanças reais desde a última versão const lastVersion = existingHistory.versions[existingHistory.versions.length - 1]; const normalizedLastContent = lastVersion?.content.replace(/\r\n/g, '\n').trim(); - + if (normalizedCurrentContent === normalizedLastContent) { return; // Não criar novo histórico se o conteúdo é o mesmo } // Verificar se há mudanças significativas usando diffFiles const relativePath = extractRelativePath(selectedFile); - const unifiedDiff = diffFiles( - relativePath, - existingHistory.originalContent, - currentContent - ); + const unifiedDiff = diffFiles(relativePath, existingHistory.originalContent, currentContent); if (unifiedDiff) { - const newChanges = diffLines( - existingHistory.originalContent, - currentContent - ); + const newChanges = diffLines(existingHistory.originalContent, currentContent); // Verificar se as mudanças são significativas - const hasSignificantChanges = newChanges.some(change => - (change.added || change.removed) && change.value.trim().length > 0 + const hasSignificantChanges = newChanges.some( + (change) => (change.added || change.removed) && change.value.trim().length > 0, ); if (hasSignificantChanges) { const newHistory: FileHistory = { originalContent: existingHistory.originalContent, lastModified: Date.now(), - changes: [ - ...existingHistory.changes, - ...newChanges - ].slice(-100), // Limitar histórico de mudanças + changes: [...existingHistory.changes, ...newChanges].slice(-100), // Limitar histórico de mudanças versions: [ ...existingHistory.versions, { timestamp: Date.now(), - content: currentContent - } + content: currentContent, + }, ].slice(-10), // Manter apenas as 10 últimas versões - changeSource: 'auto-save' + changeSource: 'auto-save', }; - - setFileHistory(prev => ({ ...prev, [selectedFile]: newHistory })); + + setFileHistory((prev) => ({ ...prev, [selectedFile]: newHistory })); } } } diff --git a/app/components/workbench/FileTree.tsx b/app/components/workbench/FileTree.tsx index 08d323e5..eed791ef 100644 --- a/app/components/workbench/FileTree.tsx +++ b/app/components/workbench/FileTree.tsx @@ -274,15 +274,19 @@ function File({ fileHistory = {}, }: FileProps) { const fileModifications = fileHistory[fullPath]; - const hasModifications = fileModifications !== undefined; + + // const hasModifications = fileModifications !== undefined; // Calculate added and removed lines from the most recent changes const { additions, deletions } = useMemo(() => { - if (!fileModifications?.originalContent) return { additions: 0, deletions: 0 }; + if (!fileModifications?.originalContent) { + return { additions: 0, deletions: 0 }; + } // Usar a mesma lógica do DiffView para processar as mudanças const normalizedOriginal = fileModifications.originalContent.replace(/\r\n/g, '\n'); - const normalizedCurrent = fileModifications.versions[fileModifications.versions.length - 1]?.content.replace(/\r\n/g, '\n') || ''; + const normalizedCurrent = + fileModifications.versions[fileModifications.versions.length - 1]?.content.replace(/\r\n/g, '\n') || ''; if (normalizedOriginal === normalizedCurrent) { return { additions: 0, deletions: 0 }; @@ -291,18 +295,23 @@ function File({ const changes = diffLines(normalizedOriginal, normalizedCurrent, { newlineIsToken: false, ignoreWhitespace: true, - ignoreCase: false + ignoreCase: false, }); - return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => { - if (change.added) { - acc.additions += change.value.split('\n').length; - } - if (change.removed) { - acc.deletions += change.value.split('\n').length; - } - return acc; - }, { additions: 0, deletions: 0 }); + return changes.reduce( + (acc: { additions: number; deletions: number }, change: Change) => { + if (change.added) { + acc.additions += change.value.split('\n').length; + } + + if (change.removed) { + acc.deletions += change.value.split('\n').length; + } + + return acc; + }, + { additions: 0, deletions: 0 }, + ); }, [fileModifications]); const showStats = additions > 0 || deletions > 0; @@ -330,17 +339,11 @@ function File({
{showStats && (
- {additions > 0 && ( - +{additions} - )} - {deletions > 0 && ( - -{deletions} - )} + {additions > 0 && +{additions}} + {deletions > 0 && -{deletions}}
)} - {unsavedChanges && ( - - )} + {unsavedChanges && }
diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index e5edfcec..42315ba2 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -5,7 +5,6 @@ import { memo, useCallback, useEffect, useState, useMemo } from 'react'; import { toast } from 'react-toastify'; import { Popover, Transition } from '@headlessui/react'; import { diffLines, type Change } from 'diff'; -import { formatDistanceToNow as formatDistance } from 'date-fns'; import { ActionRunner } from '~/lib/runtime/action-runner'; import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension'; import type { FileHistory } from '~/types/actions'; @@ -25,7 +24,6 @@ import { EditorPanel } from './EditorPanel'; import { Preview } from './Preview'; import useViewport from '~/lib/hooks'; import { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog'; -import Cookies from 'js-cookie'; interface WorkspaceProps { chatStarted?: boolean; @@ -71,403 +69,413 @@ const workbenchVariants = { }, } satisfies Variants; -const FileModifiedDropdown = memo(({ - fileHistory, - onSelectFile, -}: { - fileHistory: Record, - onSelectFile: (filePath: string) => void, -}) => { - const modifiedFiles = Object.entries(fileHistory); - const hasChanges = modifiedFiles.length > 0; - const [searchQuery, setSearchQuery] = useState(''); +const FileModifiedDropdown = memo( + ({ + fileHistory, + onSelectFile, + }: { + fileHistory: Record; + onSelectFile: (filePath: string) => void; + }) => { + const modifiedFiles = Object.entries(fileHistory); + const hasChanges = modifiedFiles.length > 0; + const [searchQuery, setSearchQuery] = useState(''); - const filteredFiles = useMemo(() => { - return modifiedFiles.filter(([filePath]) => - filePath.toLowerCase().includes(searchQuery.toLowerCase()) - ); - }, [modifiedFiles, searchQuery]); + const filteredFiles = useMemo(() => { + return modifiedFiles.filter(([filePath]) => filePath.toLowerCase().includes(searchQuery.toLowerCase())); + }, [modifiedFiles, searchQuery]); - return ( -
- - {({ open }: { open: boolean }) => ( - <> - - File Changes - {hasChanges && ( - - {modifiedFiles.length} - - )} - - - -
-
- setSearchQuery(e.target.value)} - className="w-full pl-8 pr-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor focus:outline-none focus:ring-2 focus:ring-blue-500/50" - /> -
-
+ return ( +
+ + {({ open }: { open: boolean }) => ( + <> + + File Changes + {hasChanges && ( + + {modifiedFiles.length} + + )} + + + +
+
+ setSearchQuery(e.target.value)} + className="w-full pl-8 pr-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> +
+
+
+
+ +
+ {filteredFiles.length > 0 ? ( + filteredFiles.map(([filePath, history]) => { + const extension = filePath.split('.').pop() || ''; + const language = getLanguageFromExtension(extension); + + return ( + + ); + }) + ) : ( +
+
+
+
+

+ {searchQuery ? 'No matching files' : 'No modified files'} +

+

+ {searchQuery ? 'Try another search' : 'Changes will appear here as you edit'} +

+
+ )}
-
- {filteredFiles.length > 0 ? ( - filteredFiles.map(([filePath, history]) => { - const extension = filePath.split('.').pop() || ''; - const language = getLanguageFromExtension(extension); - - return ( - - ); - }) - ) : ( -
-
-
-
-

- {searchQuery ? 'No matching files' : 'No modified files'} -

-

- {searchQuery ? 'Try another search' : 'Changes will appear here as you edit'} -

-
- )} -
-
- - {hasChanges && ( -
- -
- )} - - - - )} - -
- ); -}); - -export const Workbench = memo(({ - chatStarted, - isStreaming, - actionRunner, - metadata, - updateChatMestaData -}: WorkspaceProps) => { - renderLogger.trace('Workbench'); - - const [isSyncing, setIsSyncing] = useState(false); - const [isPushDialogOpen, setIsPushDialogOpen] = useState(false); - const [fileHistory, setFileHistory] = useState>({}); - - const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys()); - - const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0)); - const showWorkbench = useStore(workbenchStore.showWorkbench); - const selectedFile = useStore(workbenchStore.selectedFile); - const currentDocument = useStore(workbenchStore.currentDocument); - const unsavedFiles = useStore(workbenchStore.unsavedFiles); - const files = useStore(workbenchStore.files); - const selectedView = useStore(workbenchStore.currentView); - - const isSmallViewport = useViewport(1024); - - const setSelectedView = (view: WorkbenchViewType) => { - workbenchStore.currentView.set(view); - }; - - useEffect(() => { - if (hasPreview) { - setSelectedView('preview'); - } - }, [hasPreview]); - - useEffect(() => { - workbenchStore.setDocuments(files); - }, [files]); - - const onEditorChange = useCallback((update) => { - workbenchStore.setCurrentDocumentContent(update.content); - }, []); - - const onEditorScroll = useCallback((position) => { - workbenchStore.setCurrentDocumentScrollPosition(position); - }, []); - - const onFileSelect = useCallback((filePath: string | undefined) => { - workbenchStore.setSelectedFile(filePath); - }, []); - - const onFileSave = useCallback(() => { - workbenchStore.saveCurrentDocument().catch(() => { - toast.error('Failed to update file content'); - }); - }, []); - - const onFileReset = useCallback(() => { - workbenchStore.resetCurrentDocument(); - }, []); - - const handleSyncFiles = useCallback(async () => { - setIsSyncing(true); - - try { - const directoryHandle = await window.showDirectoryPicker(); - await workbenchStore.syncFiles(directoryHandle); - toast.success('Files synced successfully'); - } catch (error) { - console.error('Error syncing files:', error); - toast.error('Failed to sync files'); - } finally { - setIsSyncing(false); - } - }, []); - - const handleSelectFile = useCallback((filePath: string) => { - workbenchStore.setSelectedFile(filePath); - workbenchStore.currentView.set('diff'); - }, []); - - return ( - chatStarted && ( - -
+ +
+ )} + + + )} + +
+ ); + }, +); + +export const Workbench = memo( + ({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData }: WorkspaceProps) => { + renderLogger.trace('Workbench'); + + const [isSyncing, setIsSyncing] = useState(false); + const [isPushDialogOpen, setIsPushDialogOpen] = useState(false); + const [fileHistory, setFileHistory] = useState>({}); + + // const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys()); + + const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0)); + const showWorkbench = useStore(workbenchStore.showWorkbench); + const selectedFile = useStore(workbenchStore.selectedFile); + const currentDocument = useStore(workbenchStore.currentDocument); + const unsavedFiles = useStore(workbenchStore.unsavedFiles); + const files = useStore(workbenchStore.files); + const selectedView = useStore(workbenchStore.currentView); + + const isSmallViewport = useViewport(1024); + + const setSelectedView = (view: WorkbenchViewType) => { + workbenchStore.currentView.set(view); + }; + + useEffect(() => { + if (hasPreview) { + setSelectedView('preview'); + } + }, [hasPreview]); + + useEffect(() => { + workbenchStore.setDocuments(files); + }, [files]); + + const onEditorChange = useCallback((update) => { + workbenchStore.setCurrentDocumentContent(update.content); + }, []); + + const onEditorScroll = useCallback((position) => { + workbenchStore.setCurrentDocumentScrollPosition(position); + }, []); + + const onFileSelect = useCallback((filePath: string | undefined) => { + workbenchStore.setSelectedFile(filePath); + }, []); + + const onFileSave = useCallback(() => { + workbenchStore.saveCurrentDocument().catch(() => { + toast.error('Failed to update file content'); + }); + }, []); + + const onFileReset = useCallback(() => { + workbenchStore.resetCurrentDocument(); + }, []); + + const handleSyncFiles = useCallback(async () => { + setIsSyncing(true); + + try { + const directoryHandle = await window.showDirectoryPicker(); + await workbenchStore.syncFiles(directoryHandle); + toast.success('Files synced successfully'); + } catch (error) { + console.error('Error syncing files:', error); + toast.error('Failed to sync files'); + } finally { + setIsSyncing(false); + } + }, []); + + const handleSelectFile = useCallback((filePath: string) => { + workbenchStore.setSelectedFile(filePath); + workbenchStore.currentView.set('diff'); + }, []); + + return ( + chatStarted && ( + -
-
-
- -
- {selectedView === 'code' && ( -
- { - workbenchStore.downloadZip(); - }} - > -
- Download Code - - - {isSyncing ?
:
} - {isSyncing ? 'Syncing...' : 'Sync Files'} - - { - workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get()); - }} - > -
- Toggle Terminal - - setIsPushDialogOpen(true)}> -
- Push to GitHub - -
- )} - {selectedView === 'diff' && ( - +
+
+
+ +
+ {selectedView === 'code' && ( +
+ { + workbenchStore.downloadZip(); + }} + > +
+ Download Code + + + {isSyncing ?
:
} + {isSyncing ? 'Syncing...' : 'Sync Files'} + + { + workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get()); + }} + > +
+ Toggle Terminal + + setIsPushDialogOpen(true)}> +
+ Push to GitHub + +
+ )} + {selectedView === 'diff' && ( + + )} + { + workbenchStore.showWorkbench.set(false); + }} /> - )} - { - workbenchStore.showWorkbench.set(false); - }} - /> -
-
- - - - - - - - - +
+
+ + + + + + + + + +
-
- setIsPushDialogOpen(false)} - onPush={async (repoName, username, token) => { - try { - const commitMessage = prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit'; - await workbenchStore.pushToGitHub(repoName, commitMessage, username, token); - const repoUrl = `https://github.com/${username}/${repoName}`; - - if (updateChatMestaData && !metadata?.gitUrl) { - updateChatMestaData({ - ...(metadata || {}), - gitUrl: repoUrl, - }); + setIsPushDialogOpen(false)} + onPush={async (repoName, username, token) => { + try { + const commitMessage = prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit'; + await workbenchStore.pushToGitHub(repoName, commitMessage, username, token); + + const repoUrl = `https://github.com/${username}/${repoName}`; + + if (updateChatMestaData && !metadata?.gitUrl) { + updateChatMestaData({ + ...(metadata || {}), + gitUrl: repoUrl, + }); + } + + return repoUrl; + } catch (error) { + console.error('Error pushing to GitHub:', error); + toast.error('Failed to push to GitHub'); + throw error; } - - return repoUrl; - } catch (error) { - console.error('Error pushing to GitHub:', error); - toast.error('Failed to push to GitHub'); - throw error; - } - }} - /> - - ) - ); -}); + }} + /> + + ) + ); + }, +); // View component for rendering content with motion transitions interface ViewProps extends HTMLMotionProps<'div'> { diff --git a/app/lib/.server/llm/select-context.ts b/app/lib/.server/llm/select-context.ts index f79cfbe9..17afffd6 100644 --- a/app/lib/.server/llm/select-context.ts +++ b/app/lib/.server/llm/select-context.ts @@ -204,7 +204,10 @@ export async function selectContext(props: { } if (!filePaths.includes(fullPath)) { - throw new Error(`File ${path} is not in the list of files above.`); + logger.error(`File ${path} is not in the list of files above.`); + return; + + // throw new Error(`File ${path} is not in the list of files above.`); } if (currrentFiles.includes(path)) { @@ -218,6 +221,13 @@ export async function selectContext(props: { onFinish(resp); } + const totalFiles = Object.keys(filteredFiles).length; + logger.info(`Total files: ${totalFiles}`); + + if (totalFiles == 0) { + throw new Error(`Bolt failed to select files`); + } + return filteredFiles; // generateText({ diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts index c37ba4df..013a9a77 100644 --- a/app/lib/runtime/action-runner.ts +++ b/app/lib/runtime/action-runner.ts @@ -319,21 +319,23 @@ export class ActionRunner { const webcontainer = await this.#webcontainer; const historyPath = this.#getHistoryPath(filePath); const content = await webcontainer.fs.readFile(historyPath, 'utf-8'); + return JSON.parse(content); } catch (error) { + logger.error('Failed to get file history:', error); return null; } } async saveFileHistory(filePath: string, history: FileHistory) { - const webcontainer = await this.#webcontainer; + // const webcontainer = await this.#webcontainer; const historyPath = this.#getHistoryPath(filePath); await this.#runFileAction({ type: 'file', filePath: historyPath, content: JSON.stringify(history), - changeSource: 'auto-save' + changeSource: 'auto-save', } as any); } @@ -375,4 +377,4 @@ export class ActionRunner { output, }; } -} \ No newline at end of file +} diff --git a/app/routes/api.enhancer.ts b/app/routes/api.enhancer.ts index c1154bce..4ab54f31 100644 --- a/app/routes/api.enhancer.ts +++ b/app/routes/api.enhancer.ts @@ -3,11 +3,14 @@ import { streamText } from '~/lib/.server/llm/stream-text'; import { stripIndents } from '~/utils/stripIndent'; import type { ProviderInfo } from '~/types/model'; import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies'; +import { createScopedLogger } from '~/utils/logger'; export async function action(args: ActionFunctionArgs) { return enhancerAction(args); } +const logger = createScopedLogger('api.enhancher'); + async function enhancerAction({ context, request }: ActionFunctionArgs) { const { message, model, provider } = await request.json<{ message: string; @@ -77,8 +80,32 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) { env: context.cloudflare?.env as any, apiKeys, providerSettings, + options: { + system: + 'You are a senior software principal architect, you should help the user analyse the user query and enrich it with the necessary context and constraints to make it more specific, actionable, and effective. You should also ensure that the prompt is self-contained and uses professional language. Your response should ONLY contain the enhanced prompt text. Do not include any explanations, metadata, or wrapper tags.', + + /* + * onError: (event) => { + * throw new Response(null, { + * status: 500, + * statusText: 'Internal Server Error', + * }); + * } + */ + }, }); + (async () => { + for await (const part of result.fullStream) { + if (part.type === 'error') { + const error: any = part.error; + logger.error(error); + + return; + } + } + })(); + return new Response(result.textStream, { status: 200, headers: { diff --git a/app/types/actions.ts b/app/types/actions.ts index 041e23fc..623c4979 100644 --- a/app/types/actions.ts +++ b/app/types/actions.ts @@ -43,6 +43,7 @@ export interface FileHistory { timestamp: number; content: string; }[]; + // Novo campo para rastrear a origem das mudanças changeSource?: 'user' | 'auto-save' | 'external'; } diff --git a/app/utils/getLanguageFromExtension.ts b/app/utils/getLanguageFromExtension.ts index 1dd5d0fd..d4f04285 100644 --- a/app/utils/getLanguageFromExtension.ts +++ b/app/utils/getLanguageFromExtension.ts @@ -1,24 +1,24 @@ export const getLanguageFromExtension = (ext: string): string => { const map: Record = { - js: "javascript", - jsx: "jsx", - ts: "typescript", - tsx: "tsx", - json: "json", - html: "html", - css: "css", - py: "python", - java: "java", - rb: "ruby", - cpp: "cpp", - c: "c", - cs: "csharp", - go: "go", - rs: "rust", - php: "php", - swift: "swift", - md: "plaintext", - sh: "bash", + js: 'javascript', + jsx: 'jsx', + ts: 'typescript', + tsx: 'tsx', + json: 'json', + html: 'html', + css: 'css', + py: 'python', + java: 'java', + rb: 'ruby', + cpp: 'cpp', + c: 'c', + cs: 'csharp', + go: 'go', + rs: 'rust', + php: 'php', + swift: 'swift', + md: 'plaintext', + sh: 'bash', }; - return map[ext] || "typescript"; -}; \ No newline at end of file + return map[ext] || 'typescript'; +};