This commit is contained in:
KevIsDev 2025-06-16 12:36:19 +01:00 committed by GitHub
commit 530cd73362
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 136 additions and 247 deletions

View File

@ -25,7 +25,6 @@ import ChatAlert from './ChatAlert';
import type { ModelInfo } from '~/lib/modules/llm/types';
import ProgressCompilation from './ProgressCompilation';
import type { ProgressAnnotation } from '~/types/context';
import type { ActionRunner } from '~/lib/runtime/action-runner';
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
@ -71,7 +70,6 @@ interface BaseChatProps {
deployAlert?: DeployAlert;
clearDeployAlert?: () => void;
data?: JSONValue[] | undefined;
actionRunner?: ActionRunner;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
append?: (message: Message) => void;
@ -116,7 +114,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
supabaseAlert,
clearSupabaseAlert,
data,
actionRunner,
chatMode,
setChatMode,
append,
@ -483,12 +480,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div>
<ClientOnly>
{() => (
<Workbench
actionRunner={actionRunner ?? ({} as ActionRunner)}
chatStarted={chatStarted}
isStreaming={isStreaming}
setSelectedElement={setSelectedElement}
/>
<Workbench chatStarted={chatStarted} isStreaming={isStreaming} setSelectedElement={setSelectedElement} />
)}
</ClientOnly>
</div>

View File

@ -7,7 +7,6 @@ import { diffLines, type Change } from 'diff';
import { getHighlighter } from 'shiki';
import '~/styles/diff-view.css';
import { diffFiles, extractRelativePath } from '~/utils/diff';
import { ActionRunner } from '~/lib/runtime/action-runner';
import type { FileHistory } from '~/types/actions';
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
import { themeStore } from '~/lib/stores/theme';
@ -664,7 +663,6 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
interface DiffViewProps {
fileHistory: Record<string, FileHistory>;
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
actionRunner: ActionRunner;
}
export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => {

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 { ActionRunner } from '~/lib/runtime/action-runner';
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
import type { FileHistory } from '~/types/actions';
import { DiffView } from './DiffView';
@ -32,7 +31,6 @@ import type { ElementInfo } from './Inspector';
interface WorkspaceProps {
chatStarted?: boolean;
isStreaming?: boolean;
actionRunner: ActionRunner;
metadata?: {
gitUrl?: string;
};
@ -281,7 +279,7 @@ const FileModifiedDropdown = memo(
);
export const Workbench = memo(
({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
({ chatStarted, isStreaming, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
renderLogger.trace('Workbench');
const [isSyncing, setIsSyncing] = useState(false);
@ -486,7 +484,7 @@ 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} />
</View>
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
<Preview setSelectedElement={setSelectedElement} />

View File

@ -10,6 +10,7 @@ const logger = createScopedLogger('Terminal');
export interface TerminalRef {
reloadStyles: () => void;
getTerminal: () => XTerm | undefined;
}
export interface TerminalProps {
@ -80,6 +81,9 @@ export const Terminal = memo(
const terminal = terminalRef.current!;
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
},
getTerminal: () => {
return terminalRef.current;
},
};
}, []);

View File

@ -23,7 +23,7 @@ export const TerminalTabs = memo(() => {
const terminalToggledByShortcut = useRef(false);
const [activeTerminal, setActiveTerminal] = useState(0);
const [terminalCount, setTerminalCount] = useState(1);
const [terminalCount, setTerminalCount] = useState(0);
const addTerminal = () => {
if (terminalCount < MAX_TERMINALS) {
@ -32,6 +32,50 @@ export const TerminalTabs = memo(() => {
}
};
const closeTerminal = (index: number) => {
if (index === 0) {
return;
} // Can't close bolt terminal
const terminalRef = terminalRefs.current[index];
if (terminalRef?.getTerminal) {
const terminal = terminalRef.getTerminal();
if (terminal) {
workbenchStore.detachTerminal(terminal);
}
}
// Remove the terminal from refs
terminalRefs.current.splice(index, 1);
// Adjust terminal count and active terminal
setTerminalCount(terminalCount - 1);
// If we closed the active terminal, switch to previous one
if (activeTerminal === index) {
setActiveTerminal(Math.max(0, index - 1));
} else if (activeTerminal > index) {
setActiveTerminal(activeTerminal - 1);
}
};
useEffect(() => {
return () => {
// Cleanup all terminals on unmount
terminalRefs.current.forEach((ref, index) => {
if (index > 0 && ref?.getTerminal) {
const terminal = ref.getTerminal();
if (terminal) {
workbenchStore.detachTerminal(terminal);
}
}
});
};
}, []);
useEffect(() => {
const { current: terminal } = terminalPanelRef;
@ -125,6 +169,15 @@ export const TerminalTabs = memo(() => {
>
<div className="i-ph:terminal-window-duotone text-lg" />
Terminal {terminalCount > 1 && index}
<button
className="bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary hover:bg-transparent rounded"
onClick={(e) => {
e.stopPropagation();
closeTerminal(index);
}}
>
<div className="i-ph:x text-xs" />
</button>
</button>
</React.Fragment>
)}

View File

@ -45,6 +45,21 @@ export class TerminalStore {
}
}
async detachTerminal(terminal: ITerminal) {
const terminalIndex = this.#terminals.findIndex((t) => t.terminal === terminal);
if (terminalIndex !== -1) {
const { process } = this.#terminals[terminalIndex];
try {
process.kill();
} catch (error) {
console.warn('Failed to kill terminal process:', error);
}
this.#terminals.splice(terminalIndex, 1);
}
}
onTerminalResize(cols: number, rows: number) {
for (const { process } of this.#terminals) {
process.resize({ cols, rows });

View File

@ -147,6 +147,10 @@ export class WorkbenchStore {
this.#terminalStore.attachBoltTerminal(terminal);
}
detachTerminal(terminal: ITerminal) {
this.#terminalStore.detachTerminal(terminal);
}
onTerminalResize(cols: number, rows: number) {
this.#terminalStore.onTerminalResize(cols, rows);
}

View File

@ -62,6 +62,28 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
);
const stream = new SwitchableStream();
const abortController = new AbortController();
const activeOperations = new Set<Promise<any>>();
// Cleanup function
const cleanup = () => {
try {
// Abort any ongoing operations
abortController.abort();
// Close streams
if (stream && typeof stream.close === 'function') {
stream.close();
}
// Cancel active operations
activeOperations.clear();
logger.debug('Chat action cleanup completed');
} catch (error) {
logger.error('Error during cleanup:', error);
}
};
const cumulativeUsage = {
completionTokens: 0,
@ -298,6 +320,30 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
messageSliceId,
});
// Track async operations for cleanup
const summaryOperation = summary
? createSummary({
messages: [...messages],
env: context.cloudflare?.env,
apiKeys,
providerSettings,
promptId,
contextOptimization,
onFinish(resp) {
if (resp.usage) {
logger.debug('createSummary token usage', JSON.stringify(resp.usage));
cumulativeUsage.completionTokens += resp.usage.completionTokens || 0;
cumulativeUsage.promptTokens += resp.usage.promptTokens || 0;
cumulativeUsage.totalTokens += resp.usage.totalTokens || 0;
}
},
})
: Promise.resolve();
if (summaryOperation) {
activeOperations.add(summaryOperation);
}
(async () => {
for await (const part of result.fullStream) {
if (part.type === 'error') {
@ -310,7 +356,10 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
})();
result.mergeIntoDataStream(dataStream);
},
onError: (error: any) => `Custom error: ${error.message}`,
onError: (error: any) => {
cleanup();
return `Custom error: ${error.message}`;
},
}).pipeThrough(
new TransformStream({
transform: (chunk, controller) => {
@ -359,6 +408,9 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
},
});
} catch (error: any) {
// Ensure cleanup on error
cleanup();
logger.error(error);
if (error.message?.includes('API key')) {

View File

@ -1,229 +0,0 @@
import type { WebContainer } from '@webcontainer/api';
import { WORK_DIR } from './constants';
// Global object to track watcher state
const watcherState = {
fallbackEnabled: tryLoadFallbackState(),
watchingPaths: new Set<string>(),
callbacks: new Map<string, Set<() => void>>(),
pollingInterval: null as NodeJS.Timeout | null,
};
// Try to load the fallback state from localStorage
function tryLoadFallbackState(): boolean {
try {
if (typeof localStorage !== 'undefined') {
const state = localStorage.getItem('bolt-file-watcher-fallback');
return state === 'true';
}
} catch {
console.warn('[FileWatcher] Failed to load fallback state from localStorage');
}
return false;
}
// Save the fallback state to localStorage
function saveFallbackState(state: boolean) {
try {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('bolt-file-watcher-fallback', state ? 'true' : 'false');
}
} catch {
console.warn('[FileWatcher] Failed to save fallback state to localStorage');
}
}
/**
* Safe file watcher that falls back to polling when native file watching fails
*
* @param webcontainer The WebContainer instance
* @param pattern File pattern to watch
* @param callback Function to call when files change
* @returns An object with a close method
*/
export async function safeWatch(webcontainer: WebContainer, pattern: string = '**/*', callback: () => void) {
// Register the callback
if (!watcherState.callbacks.has(pattern)) {
watcherState.callbacks.set(pattern, new Set());
}
watcherState.callbacks.get(pattern)!.add(callback);
// If we're already using fallback mode, don't try native watchers again
if (watcherState.fallbackEnabled) {
// Make sure polling is active
ensurePollingActive();
// Return a cleanup function
return {
close: () => {
const callbacks = watcherState.callbacks.get(pattern);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
watcherState.callbacks.delete(pattern);
}
}
},
};
}
// Try to use native file watching
try {
const watcher = await webcontainer.fs.watch(pattern, { persistent: true });
watcherState.watchingPaths.add(pattern);
// Use the native watch events
(watcher as any).addEventListener('change', () => {
// Call all callbacks for this pattern
const callbacks = watcherState.callbacks.get(pattern);
if (callbacks) {
callbacks.forEach((cb) => cb());
}
});
// Return an object with a close method
return {
close: () => {
try {
watcher.close();
watcherState.watchingPaths.delete(pattern);
const callbacks = watcherState.callbacks.get(pattern);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
watcherState.callbacks.delete(pattern);
}
}
} catch (error) {
console.warn('[FileWatcher] Error closing watcher:', error);
}
},
};
} catch (error) {
console.warn('[FileWatcher] Native file watching failed:', error);
console.info('[FileWatcher] Falling back to polling mechanism for file changes');
// Switch to fallback mode for all future watches
watcherState.fallbackEnabled = true;
saveFallbackState(true);
// Start polling
ensurePollingActive();
// Return a mock watcher object
return {
close: () => {
const callbacks = watcherState.callbacks.get(pattern);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
watcherState.callbacks.delete(pattern);
}
}
// If no more callbacks, stop polling
if (watcherState.callbacks.size === 0 && watcherState.pollingInterval) {
clearInterval(watcherState.pollingInterval);
watcherState.pollingInterval = null;
}
},
};
}
}
// Ensure polling is active
function ensurePollingActive() {
if (watcherState.pollingInterval) {
return;
}
// Set up a polling interval that calls all callbacks
watcherState.pollingInterval = setInterval(() => {
// Call all registered callbacks
for (const [, callbacks] of watcherState.callbacks.entries()) {
callbacks.forEach((callback) => callback());
}
}, 3000); // Poll every 3 seconds
// Clean up interval when window unloads
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
if (watcherState.pollingInterval) {
clearInterval(watcherState.pollingInterval);
watcherState.pollingInterval = null;
}
});
}
}
// SafeWatchPaths mimics the webcontainer.internal.watchPaths method but with fallback
export function safeWatchPaths(
webcontainer: WebContainer,
config: { include: string[]; exclude?: string[]; includeContent?: boolean },
callback: any,
) {
// Create a valid mock event to prevent undefined errors
const createMockEvent = () => ({
type: 'change',
path: `${WORK_DIR}/mock-path.txt`,
buffer: new Uint8Array(0),
});
// Start with polling if we already know native watching doesn't work
if (watcherState.fallbackEnabled) {
console.info('[FileWatcher] Using fallback polling for watchPaths');
ensurePollingActive();
const interval = setInterval(() => {
// Use our helper to create a valid event
const mockEvent = createMockEvent();
// Wrap in the expected structure of nested arrays
callback([[mockEvent]]);
}, 3000);
return {
close: () => {
clearInterval(interval);
},
};
}
// Try native watching
try {
return webcontainer.internal.watchPaths(config, callback);
} catch (error) {
console.warn('[FileWatcher] Native watchPaths failed:', error);
console.info('[FileWatcher] Using fallback polling for watchPaths');
// Mark as using fallback
watcherState.fallbackEnabled = true;
saveFallbackState(true);
// Set up polling
ensurePollingActive();
const interval = setInterval(() => {
// Use our helper to create a valid event
const mockEvent = createMockEvent();
// Wrap in the expected structure of nested arrays
callback([[mockEvent]]);
}, 3000);
return {
close: () => {
clearInterval(interval);
},
};
}
}

View File

@ -4,7 +4,9 @@ import type { Template } from '~/types/template';
import { STARTER_TEMPLATES } from './constants';
const starterTemplateSelectionPrompt = (templates: Template[]) => `
You are an experienced developer who helps people choose the best starter template for their projects, Vite is preferred.
You are an experienced developer who helps people choose the best starter template for their projects.
IMPORTANT: Vite is preferred
IMPORTANT: Only choose shadcn templates if the user explicitly asks for shadcn.
Available templates:
<template>