Revert "fix: resolve chat conversation hanging and stream interruption issues (#1971)"

This reverts commit e68593f22d.
This commit is contained in:
Stijnus
2025-09-07 00:14:13 +02:00
committed by Stijnus
parent e68593f22d
commit 37217a5c7b
61 changed files with 1432 additions and 8811 deletions

View File

@@ -13,7 +13,6 @@ import {
type OnScrollCallback as OnEditorScroll,
} from '~/components/editor/codemirror/CodeMirrorEditor';
import { IconButton } from '~/components/ui/IconButton';
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
import { Slider, type SliderOptions } from '~/components/ui/Slider';
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
@@ -23,11 +22,13 @@ import { EditorPanel } from './EditorPanel';
import { Preview } from './Preview';
import useViewport from '~/lib/hooks';
// import { GitLabDeploymentDialog } from '~/components/deploy/GitLabDeploymentDialog';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { usePreviewStore } from '~/lib/stores/previews';
import { chatStore } from '~/lib/stores/chat';
import type { ElementInfo } from './Inspector';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { useChatHistory } from '~/lib/persistence';
import { streamingState } from '~/lib/stores/streaming';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
interface WorkspaceProps {
chatStarted?: boolean;
@@ -279,298 +280,239 @@ const FileModifiedDropdown = memo(
},
);
export const Workbench = memo(({ chatStarted, isStreaming, setSelectedElement }: WorkspaceProps) => {
renderLogger.trace('Workbench');
export const Workbench = memo(
({
chatStarted,
isStreaming,
metadata: _metadata,
updateChatMestaData: _updateChatMestaData,
setSelectedElement,
}: WorkspaceProps) => {
renderLogger.trace('Workbench');
const [isSyncing, setIsSyncing] = useState(false);
const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
// const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
// const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
// Keyboard shortcut for Save All (Ctrl+Shift+S)
useEffect(() => {
const handleKeyPress = async (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 's') {
e.preventDefault();
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 { showChat } = useStore(chatStore);
const canHideChat = showWorkbench || !showChat;
const unsavedFiles = workbenchStore.unsavedFiles.get();
const isSmallViewport = useViewport(1024);
const streaming = useStore(streamingState);
const { exportChat } = useChatHistory();
const [isSyncing, setIsSyncing] = useState(false);
if (unsavedFiles.size > 0) {
try {
await workbenchStore.saveAllFiles();
toast.success(`Saved ${unsavedFiles.size} file${unsavedFiles.size > 1 ? 's' : ''}`, {
position: 'bottom-right',
autoClose: 2000,
});
} catch {
toast.error('Failed to save some files', {
position: 'bottom-right',
autoClose: 3000,
});
}
} else {
toast.info('All files are already saved', {
position: 'bottom-right',
autoClose: 2000,
});
}
}
const setSelectedView = (view: WorkbenchViewType) => {
workbenchStore.currentView.set(view);
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
useEffect(() => {
if (hasPreview) {
setSelectedView('preview');
}
}, [hasPreview]);
// const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
useEffect(() => {
workbenchStore.setDocuments(files);
}, [files]);
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 { showChat } = useStore(chatStore);
const canHideChat = showWorkbench || !showChat;
const onEditorChange = useCallback<OnEditorChange>((update) => {
workbenchStore.setCurrentDocumentContent(update.content);
}, []);
const isSmallViewport = useViewport(1024);
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
workbenchStore.setCurrentDocumentScrollPosition(position);
}, []);
const setSelectedView = (view: WorkbenchViewType) => {
workbenchStore.currentView.set(view);
};
const onFileSelect = useCallback((filePath: string | undefined) => {
workbenchStore.setSelectedFile(filePath);
}, []);
useEffect(() => {
if (hasPreview) {
setSelectedView('preview');
}
}, [hasPreview]);
const onFileSave = useCallback(() => {
workbenchStore
.saveCurrentDocument()
.then(() => {
// Explicitly refresh all previews after a file save
const previewStore = usePreviewStore();
previewStore.refreshAllPreviews();
})
.catch(() => {
toast.error('Failed to update file content');
});
}, []);
useEffect(() => {
workbenchStore.setDocuments(files);
}, [files]);
const onFileReset = useCallback(() => {
workbenchStore.resetCurrentDocument();
}, []);
const onEditorChange = useCallback<OnEditorChange>((update) => {
workbenchStore.setCurrentDocumentContent(update.content);
}, []);
const handleSelectFile = useCallback((filePath: string) => {
workbenchStore.setSelectedFile(filePath);
workbenchStore.currentView.set('diff');
}, []);
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
workbenchStore.setCurrentDocumentScrollPosition(position);
}, []);
const handleSyncFiles = useCallback(async () => {
setIsSyncing(true);
const onFileSelect = useCallback((filePath: string | undefined) => {
workbenchStore.setSelectedFile(filePath);
}, []);
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 onFileSave = useCallback(() => {
workbenchStore
.saveCurrentDocument()
.then(() => {
// Explicitly refresh all previews after a file save
const previewStore = usePreviewStore();
previewStore.refreshAllPreviews();
})
.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 {
console.error('Error syncing files');
toast.error('Failed to sync files');
} finally {
setIsSyncing(false);
}
}, []);
const handleSelectFile = useCallback((filePath: string) => {
workbenchStore.setSelectedFile(filePath);
workbenchStore.currentView.set('diff');
}, []);
return (
chatStarted && (
<motion.div
initial="closed"
animate={showWorkbench ? 'open' : 'closed'}
variants={workbenchVariants}
className="z-workbench"
>
<div
className={classNames(
'fixed top-[calc(var(--header-height)+1.2rem)] bottom-6 w-[var(--workbench-inner-width)] z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
{
'w-full': isSmallViewport,
'left-0': showWorkbench && isSmallViewport,
'left-[var(--workbench-left)]': showWorkbench,
'left-[100%]': !showWorkbench,
},
)}
return (
chatStarted && (
<motion.div
initial="closed"
animate={showWorkbench ? 'open' : 'closed'}
variants={workbenchVariants}
className="z-workbench"
>
<div className="absolute inset-0 px-2 lg:px-4">
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor gap-1.5">
<button
className={`${showChat ? 'i-ph:sidebar-simple-fill' : 'i-ph:sidebar-simple'} text-lg text-bolt-elements-textSecondary mr-1`}
disabled={!canHideChat || isSmallViewport}
onClick={() => {
if (canHideChat) {
chatStore.setKey('showChat', !showChat);
}
}}
/>
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
<div className="ml-auto" />
{selectedView === 'code' && (
<div className="flex overflow-y-auto">
<PanelHeaderButton
className="mr-1 text-sm"
onClick={async () => {
console.log('[SaveAll] Button clicked');
const unsavedFiles = workbenchStore.unsavedFiles.get();
console.log('[SaveAll] Unsaved files:', Array.from(unsavedFiles));
if (unsavedFiles.size > 0) {
try {
console.log('[SaveAll] Starting save...');
await workbenchStore.saveAllFiles();
toast.success(`Saved ${unsavedFiles.size} file${unsavedFiles.size > 1 ? 's' : ''}`, {
position: 'bottom-right',
autoClose: 2000,
});
console.log('[SaveAll] Save successful');
} catch {
console.error('[SaveAll] Save failed');
toast.error('Failed to save files', {
position: 'bottom-right',
autoClose: 3000,
});
}
} else {
console.log('[SaveAll] No unsaved files');
toast.info('All files are already saved', {
position: 'bottom-right',
autoClose: 2000,
});
}
}}
>
<div className="i-ph:floppy-disk" />
Save All
</PanelHeaderButton>
<PanelHeaderButton
className="mr-1 text-sm"
onClick={() => {
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
}}
>
<div className="i-ph:terminal" />
Toggle Terminal
</PanelHeaderButton>
<DropdownMenu.Root>
<DropdownMenu.Trigger className="text-sm flex items-center gap-1 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed">
<div className="i-ph:box-arrow-up" />
Sync
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'min-w-[240px] z-[250]',
'bg-white dark:bg-[#141414]',
'rounded-lg shadow-lg',
'border border-gray-200/50 dark:border-gray-800/50',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={handleSyncFiles}
disabled={isSyncing}
>
<div className="flex items-center gap-2">
{isSyncing ? <div className="i-ph:spinner" /> : <div className="i-ph:cloud-arrow-down" />}
<span>{isSyncing ? 'Syncing...' : 'Sync Files'}</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={() => {
/* GitHub push temporarily disabled */
}}
>
<div className="flex items-center gap-2">
<div className="i-ph:git-branch" />
Push to GitHub
</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
)}
{selectedView === 'diff' && (
<FileModifiedDropdown fileHistory={fileHistory} onSelectFile={handleSelectFile} />
)}
<IconButton
icon="i-ph:x-circle"
className="-mr-1"
size="xl"
onClick={() => {
workbenchStore.showWorkbench.set(false);
}}
/>
</div>
<div className="relative flex-1 overflow-hidden">
<View initial={{ x: '0%' }} animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}>
<EditorPanel
editorDocument={currentDocument}
isStreaming={isStreaming}
selectedFile={selectedFile}
files={files}
unsavedFiles={unsavedFiles}
fileHistory={fileHistory}
onFileSelect={onFileSelect}
onEditorScroll={onEditorScroll}
onEditorChange={onEditorChange}
onFileSave={onFileSave}
onFileReset={onFileReset}
<div
className={classNames(
'fixed top-[calc(var(--header-height)+1.2rem)] bottom-6 w-[var(--workbench-inner-width)] z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
{
'w-full': isSmallViewport,
'left-0': showWorkbench && isSmallViewport,
'left-[var(--workbench-left)]': showWorkbench,
'left-[100%]': !showWorkbench,
},
)}
>
<div className="absolute inset-0 px-2 lg:px-4">
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor gap-1.5">
<button
className={`${showChat ? 'i-ph:sidebar-simple-fill' : 'i-ph:sidebar-simple'} text-lg text-bolt-elements-textSecondary mr-1`}
disabled={!canHideChat || isSmallViewport}
onClick={() => {
if (canHideChat) {
chatStore.setKey('showChat', !showChat);
}
}}
/>
</View>
<View
initial={{ x: '100%' }}
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
>
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} />
</View>
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
<Preview setSelectedElement={setSelectedElement} />
</View>
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
<div className="ml-auto" />
{selectedView === 'code' && (
<div className="flex overflow-y-auto">
{/* Export Chat Button */}
<ExportChatButton exportChat={exportChat} />
{/* Sync Button */}
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden ml-1">
<DropdownMenu.Root>
<DropdownMenu.Trigger
disabled={isSyncing || streaming}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
{isSyncing ? 'Syncing...' : 'Sync'}
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'min-w-[240px] z-[250]',
'bg-white dark:bg-[#141414]',
'rounded-lg shadow-lg',
'border border-gray-200/50 dark:border-gray-800/50',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={handleSyncFiles}
disabled={isSyncing}
>
<div className="flex items-center gap-2">
{isSyncing ? (
<div className="i-ph:spinner" />
) : (
<div className="i-ph:cloud-arrow-down" />
)}
<span>{isSyncing ? 'Syncing...' : 'Sync Files'}</span>
</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/* Toggle Terminal Button */}
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden ml-1">
<button
onClick={() => {
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
}}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
<div className="i-ph:terminal" />
Toggle Terminal
</button>
</div>
</div>
)}
{selectedView === 'diff' && (
<FileModifiedDropdown fileHistory={fileHistory} onSelectFile={handleSelectFile} />
)}
<IconButton
icon="i-ph:x-circle"
className="-mr-1"
size="xl"
onClick={() => {
workbenchStore.showWorkbench.set(false);
}}
/>
</div>
<div className="relative flex-1 overflow-hidden">
<View initial={{ x: '0%' }} animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}>
<EditorPanel
editorDocument={currentDocument}
isStreaming={isStreaming}
selectedFile={selectedFile}
files={files}
unsavedFiles={unsavedFiles}
fileHistory={fileHistory}
onFileSelect={onFileSelect}
onEditorScroll={onEditorScroll}
onEditorChange={onEditorChange}
onFileSave={onFileSave}
onFileReset={onFileReset}
/>
</View>
<View
initial={{ x: '100%' }}
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
>
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} />
</View>
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
<Preview setSelectedElement={setSelectedElement} />
</View>
</div>
</div>
</div>
</div>
</div>
{/* GitHub push dialog temporarily disabled during merge - will be re-enabled with new GitLab integration */}
</motion.div>
)
);
});
</motion.div>
)
);
},
);
// View component for rendering content with motion transitions
interface ViewProps extends HTMLMotionProps<'div'> {