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
This commit is contained in:
Anirban Kar 2025-03-01 01:34:35 +05:30 committed by GitHub
parent b98485d99f
commit 3c28e8ad88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 758 additions and 685 deletions

View File

@ -4,11 +4,6 @@ import { classNames } from '~/utils/classNames';
import { cubicEasingFn } from '~/utils/easings';
import { genericMemo } from '~/utils/react';
interface SliderOption<T> {
value: T;
text: string;
}
export type SliderOptions<T> = {
left: { value: T; text: string };
middle?: { value: T; text: string };
@ -38,7 +33,10 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
</SliderButton>
)}
<SliderButton selected={!isLeftSelected && !isMiddleSelected} setSelected={() => setSelected?.(options.right.value)}>
<SliderButton
selected={!isLeftSelected && !isMiddleSelected}
setSelected={() => setSelected?.(options.right.value)}
>
{options.right.text}
</SliderButton>
</div>

View File

@ -41,14 +41,16 @@ const FullscreenButton = memo(({ onClick, isFullscreen }: FullscreenButtonProps)
<button
onClick={onClick}
className="ml-4 p-1 rounded hover:bg-bolt-elements-background-depth-3 text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-colors"
title={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
title={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
>
<div className={isFullscreen ? "i-ph:corners-in" : "i-ph:corners-out"} />
<div className={isFullscreen ? 'i-ph:corners-in' : 'i-ph:corners-out'} />
</button>
));
const FullscreenOverlay = memo(({ isFullscreen, children }: { isFullscreen: boolean; children: React.ReactNode }) => {
if (!isFullscreen) return <>{children}</>;
if (!isFullscreen) {
return <>{children}</>;
}
return (
<div className="fixed inset-0 z-[9999] bg-black/50 flex items-center justify-center p-6">
@ -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<number>(),
after: new Set<number>()
after: new Set<number>(),
};
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;
@ -170,17 +174,22 @@ const processChanges = (beforeCode: string, afterCode: string) => {
// Find common prefix and suffix
let prefixLength = 0;
while (prefixLength < beforeLine.length &&
while (
prefixLength < beforeLine.length &&
prefixLength < afterLine.length &&
beforeLine[prefixLength] === afterLine[prefixLength]) {
beforeLine[prefixLength] === afterLine[prefixLength]
) {
prefixLength++;
}
let suffixLength = 0;
while (suffixLength < beforeLine.length - prefixLength &&
while (
suffixLength < beforeLine.length - prefixLength &&
suffixLength < afterLine.length - prefixLength &&
beforeLine[beforeLine.length - 1 - suffixLength] ===
afterLine[afterLine.length - 1 - suffixLength]) {
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,20 +339,24 @@ const renderContentWarning = (type: 'binary' | 'error') => (
{type === 'binary' ? 'Binary file detected' : 'Error processing file'}
</p>
<p className="text-sm mt-1">
{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'}
</p>
</div>
</div>
);
const NoChangesView = memo(({ beforeCode, language, highlighter, theme }: {
const NoChangesView = memo(
({
beforeCode,
language,
highlighter,
theme,
}: {
beforeCode: string;
language: string;
highlighter: any;
theme: string;
}) => (
}) => (
<div className="h-full flex flex-col items-center justify-center p-4">
<div className="text-center text-bolt-elements-textTertiary">
<div className="i-ph:files text-4xl text-green-400 mb-2 mx-auto" />
@ -355,20 +373,27 @@ const NoChangesView = memo(({ beforeCode, language, highlighter, theme }: {
<div className={lineNumberStyles}>{index + 1}</div>
<div className={lineContentStyles}>
<span className="mr-2"> </span>
<span dangerouslySetInnerHTML={{
__html: highlighter ?
highlighter.codeToHtml(line, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' })
<span
dangerouslySetInnerHTML={{
__html: highlighter
? highlighter
.codeToHtml(line, {
lang: language,
theme: theme === 'dark' ? 'github-dark' : 'github-light',
})
.replace(/<\/?pre[^>]*>/g, '')
.replace(/<\/?code[^>]*>/g, '')
: line
}} />
: line,
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
));
),
);
// Otimização do processamento de diferenças com memoização
const useProcessChanges = (beforeCode: string, afterCode: string) => {
@ -376,15 +401,16 @@ const useProcessChanges = (beforeCode: string, afterCode: string) => {
};
// Componente otimizado para renderização de linhas de código
const CodeLine = memo(({
const CodeLine = memo(
({
lineNumber,
content,
type,
highlighter,
language,
block,
theme
}: {
theme,
}: {
lineNumber: number;
content: string;
type: 'added' | 'removed' | 'unchanged';
@ -392,13 +418,14 @@ const CodeLine = memo(({
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' })
const highlightedCode = highlighter
? highlighter
.codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' })
.replace(/<\/?pre[^>]*>/g, '')
.replace(/<\/?code[^>]*>/g, '')
: content;
@ -410,19 +437,17 @@ const CodeLine = memo(({
{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' })
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 (
<span
key={index}
className={changeClass}
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
);
return <span key={index} className={changeClass} dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
})}
</>
);
@ -441,43 +466,52 @@ const CodeLine = memo(({
</div>
</div>
);
});
},
);
// Componente para exibir informações sobre o arquivo
const FileInfo = memo(({
const FileInfo = memo(
({
filename,
hasChanges,
onToggleFullscreen,
isFullscreen,
beforeCode,
afterCode
}: {
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 (!hasChanges) {
return { additions: 0, deletions: 0 };
}
const changes = diffLines(beforeCode, afterCode, {
newlineIsToken: false,
ignoreWhitespace: true,
ignoreCase: false
ignoreCase: false,
});
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
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 });
},
{ additions: 0, deletions: 0 },
);
}, [hasChanges, beforeCode, afterCode]);
const showStats = additions > 0 || deletions > 0;
@ -491,18 +525,12 @@ const FileInfo = memo(({
<>
{showStats && (
<div className="flex items-center gap-1 text-xs">
{additions > 0 && (
<span className="text-green-700 dark:text-green-500">+{additions}</span>
)}
{deletions > 0 && (
<span className="text-red-700 dark:text-red-500">-{deletions}</span>
)}
{additions > 0 && <span className="text-green-700 dark:text-green-500">+{additions}</span>}
{deletions > 0 && <span className="text-red-700 dark:text-red-500">-{deletions}</span>}
</div>
)}
<span className="text-yellow-600 dark:text-yellow-400">Modified</span>
<span className="text-bolt-elements-textTertiary text-xs">
{new Date().toLocaleTimeString()}
</span>
<span className="text-bolt-elements-textTertiary text-xs">{new Date().toLocaleTimeString()}</span>
</>
) : (
<span className="text-green-700 dark:text-green-400">No Changes</span>
@ -511,15 +539,16 @@ const FileInfo = memo(({
</span>
</div>
);
});
},
);
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, lightTheme, darkTheme }: CodeComparisonProps) => {
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [highlighter, setHighlighter] = useState<any>(null);
const theme = useStore(themeStore);
const toggleFullscreen = useCallback(() => {
setIsFullscreen(prev => !prev);
setIsFullscreen((prev) => !prev);
}, []);
const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode);
@ -527,11 +556,13 @@ 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 (
<FullscreenOverlay isFullscreen={isFullscreen}>
@ -561,12 +592,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
))}
</div>
) : (
<NoChangesView
beforeCode={beforeCode}
language={language}
highlighter={highlighter}
theme={theme}
/>
<NoChangesView beforeCode={beforeCode} language={language} highlighter={highlighter} theme={theme} />
)}
</div>
</div>
@ -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,33 +615,41 @@ 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: [{
versions: [
{
timestamp: Date.now(),
content: currentContent
}],
changeSource: 'auto-save'
}
content: currentContent,
},
],
changeSource: 'auto-save',
},
}));
}
return;
}
@ -629,42 +663,32 @@ export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: Dif
// 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 }));
}
}
}

View File

@ -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) => {
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 });
},
{ additions: 0, deletions: 0 },
);
}, [fileModifications]);
const showStats = additions > 0 || deletions > 0;
@ -330,17 +339,11 @@ function File({
<div className="flex items-center gap-1">
{showStats && (
<div className="flex items-center gap-1 text-xs">
{additions > 0 && (
<span className="text-green-500">+{additions}</span>
)}
{deletions > 0 && (
<span className="text-red-500">-{deletions}</span>
)}
{additions > 0 && <span className="text-green-500">+{additions}</span>}
{deletions > 0 && <span className="text-red-500">-{deletions}</span>}
</div>
)}
{unsavedChanges && (
<span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />
)}
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
</div>
</div>
</NodeButton>

View File

@ -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,21 +69,20 @@ const workbenchVariants = {
},
} satisfies Variants;
const FileModifiedDropdown = memo(({
const FileModifiedDropdown = memo(
({
fileHistory,
onSelectFile,
}: {
fileHistory: Record<string, FileHistory>,
onSelectFile: (filePath: string) => void,
}) => {
}: {
fileHistory: Record<string, FileHistory>;
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())
);
return modifiedFiles.filter(([filePath]) => filePath.toLowerCase().includes(searchQuery.toLowerCase()));
}, [modifiedFiles, searchQuery]);
return (
@ -139,7 +136,9 @@ const FileModifiedDropdown = memo(({
>
<div className="flex items-center gap-2">
<div className="shrink-0 w-5 h-5 text-bolt-elements-textTertiary">
{['typescript', 'javascript', 'jsx', 'tsx'].includes(language) && <div className="i-ph:file-js" />}
{['typescript', 'javascript', 'jsx', 'tsx'].includes(language) && (
<div className="i-ph:file-js" />
)}
{['css', 'scss', 'less'].includes(language) && <div className="i-ph:paint-brush" />}
{language === 'html' && <div className="i-ph:code" />}
{language === 'json' && <div className="i-ph:brackets-curly" />}
@ -149,7 +148,24 @@ const FileModifiedDropdown = memo(({
{language === 'sql' && <div className="i-ph:database" />}
{language === 'dockerfile' && <div className="i-ph:cube" />}
{language === 'shell' && <div className="i-ph:terminal" />}
{!['typescript', 'javascript', 'css', 'html', 'json', 'python', 'markdown', 'yaml', 'yml', 'sql', 'dockerfile', 'shell', 'jsx', 'tsx', 'scss', 'less'].includes(language) && <div className="i-ph:file-text" />}
{![
'typescript',
'javascript',
'css',
'html',
'json',
'python',
'markdown',
'yaml',
'yml',
'sql',
'dockerfile',
'shell',
'jsx',
'tsx',
'scss',
'less',
].includes(language) && <div className="i-ph:file-text" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
@ -164,10 +180,16 @@ const FileModifiedDropdown = memo(({
{(() => {
// Calculate diff stats
const { additions, deletions } = (() => {
if (!history.originalContent) return { additions: 0, deletions: 0 };
if (!history.originalContent) {
return { additions: 0, deletions: 0 };
}
const normalizedOriginal = history.originalContent.replace(/\r\n/g, '\n');
const normalizedCurrent = history.versions[history.versions.length - 1]?.content.replace(/\r\n/g, '\n') || '';
const normalizedCurrent =
history.versions[history.versions.length - 1]?.content.replace(
/\r\n/g,
'\n',
) || '';
if (normalizedOriginal === normalizedCurrent) {
return { additions: 0, deletions: 0 };
@ -176,31 +198,34 @@ const FileModifiedDropdown = memo(({
const changes = diffLines(normalizedOriginal, normalizedCurrent, {
newlineIsToken: false,
ignoreWhitespace: true,
ignoreCase: false
ignoreCase: false,
});
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
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 });
},
{ additions: 0, deletions: 0 },
);
})();
const showStats = additions > 0 || deletions > 0;
return showStats && (
return (
showStats && (
<div className="flex items-center gap-1 text-xs shrink-0">
{additions > 0 && (
<span className="text-green-500">+{additions}</span>
)}
{deletions > 0 && (
<span className="text-red-500">-{deletions}</span>
)}
{additions > 0 && <span className="text-green-500">+{additions}</span>}
{deletions > 0 && <span className="text-red-500">-{deletions}</span>}
</div>
)
);
})()}
</div>
@ -229,11 +254,9 @@ const FileModifiedDropdown = memo(({
<div className="border-t border-bolt-elements-borderColor p-2">
<button
onClick={() => {
navigator.clipboard.writeText(
filteredFiles.map(([filePath]) => filePath).join('\n')
);
navigator.clipboard.writeText(filteredFiles.map(([filePath]) => filePath).join('\n'));
toast('File list copied to clipboard', {
icon: <div className="i-ph:check-circle text-accent-500" />
icon: <div className="i-ph:check-circle text-accent-500" />,
});
}}
className="w-full flex items-center justify-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-1 hover:bg-bolt-elements-background-depth-3 transition-colors text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary"
@ -249,22 +272,18 @@ const FileModifiedDropdown = memo(({
</Popover>
</div>
);
});
},
);
export const Workbench = memo(({
chatStarted,
isStreaming,
actionRunner,
metadata,
updateChatMestaData
}: WorkspaceProps) => {
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<Record<string, FileHistory>>({});
const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
// const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
const showWorkbench = useStore(workbenchStore.showWorkbench);
@ -387,10 +406,7 @@ export const Workbench = memo(({
</div>
)}
{selectedView === 'diff' && (
<FileModifiedDropdown
fileHistory={fileHistory}
onSelectFile={handleSelectFile}
/>
<FileModifiedDropdown fileHistory={fileHistory} onSelectFile={handleSelectFile} />
)}
<IconButton
icon="i-ph:x-circle"
@ -402,10 +418,7 @@ export const Workbench = memo(({
/>
</div>
<div className="relative flex-1 overflow-hidden">
<View
initial={{ x: '0%' }}
animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}
>
<View initial={{ x: '0%' }} animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}>
<EditorPanel
editorDocument={currentDocument}
isStreaming={isStreaming}
@ -424,16 +437,9 @@ export const Workbench = memo(({
initial={{ x: '100%' }}
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
>
<DiffView
fileHistory={fileHistory}
setFileHistory={setFileHistory}
actionRunner={actionRunner}
/>
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} actionRunner={actionRunner} />
</View>
<View
initial={{ x: '100%' }}
animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}
>
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
<Preview />
</View>
</div>
@ -447,6 +453,7 @@ export const Workbench = memo(({
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) {
@ -467,7 +474,8 @@ export const Workbench = memo(({
</motion.div>
)
);
});
},
);
// View component for rendering content with motion transitions
interface ViewProps extends HTMLMotionProps<'div'> {

View File

@ -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({

View File

@ -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);
}

View File

@ -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: {

View File

@ -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';
}

View File

@ -1,24 +1,24 @@
export const getLanguageFromExtension = (ext: string): string => {
const map: Record<string, string> = {
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";
return map[ext] || 'typescript';
};