mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-23 02:16:08 +00:00
Merge e750cff69c
into d0d9818964
This commit is contained in:
commit
530cd73362
@ -25,7 +25,6 @@ import ChatAlert from './ChatAlert';
|
|||||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||||
import ProgressCompilation from './ProgressCompilation';
|
import ProgressCompilation from './ProgressCompilation';
|
||||||
import type { ProgressAnnotation } from '~/types/context';
|
import type { ProgressAnnotation } from '~/types/context';
|
||||||
import type { ActionRunner } from '~/lib/runtime/action-runner';
|
|
||||||
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
|
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
|
||||||
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
|
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
@ -71,7 +70,6 @@ interface BaseChatProps {
|
|||||||
deployAlert?: DeployAlert;
|
deployAlert?: DeployAlert;
|
||||||
clearDeployAlert?: () => void;
|
clearDeployAlert?: () => void;
|
||||||
data?: JSONValue[] | undefined;
|
data?: JSONValue[] | undefined;
|
||||||
actionRunner?: ActionRunner;
|
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
append?: (message: Message) => void;
|
append?: (message: Message) => void;
|
||||||
@ -116,7 +114,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
supabaseAlert,
|
supabaseAlert,
|
||||||
clearSupabaseAlert,
|
clearSupabaseAlert,
|
||||||
data,
|
data,
|
||||||
actionRunner,
|
|
||||||
chatMode,
|
chatMode,
|
||||||
setChatMode,
|
setChatMode,
|
||||||
append,
|
append,
|
||||||
@ -483,12 +480,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
</div>
|
</div>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
{() => (
|
{() => (
|
||||||
<Workbench
|
<Workbench chatStarted={chatStarted} isStreaming={isStreaming} setSelectedElement={setSelectedElement} />
|
||||||
actionRunner={actionRunner ?? ({} as ActionRunner)}
|
|
||||||
chatStarted={chatStarted}
|
|
||||||
isStreaming={isStreaming}
|
|
||||||
setSelectedElement={setSelectedElement}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,6 @@ import { diffLines, type Change } from 'diff';
|
|||||||
import { getHighlighter } from 'shiki';
|
import { getHighlighter } from 'shiki';
|
||||||
import '~/styles/diff-view.css';
|
import '~/styles/diff-view.css';
|
||||||
import { diffFiles, extractRelativePath } from '~/utils/diff';
|
import { diffFiles, extractRelativePath } from '~/utils/diff';
|
||||||
import { ActionRunner } from '~/lib/runtime/action-runner';
|
|
||||||
import type { FileHistory } from '~/types/actions';
|
import type { FileHistory } from '~/types/actions';
|
||||||
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
|
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
|
||||||
import { themeStore } from '~/lib/stores/theme';
|
import { themeStore } from '~/lib/stores/theme';
|
||||||
@ -664,7 +663,6 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
|
|||||||
interface DiffViewProps {
|
interface DiffViewProps {
|
||||||
fileHistory: Record<string, FileHistory>;
|
fileHistory: Record<string, FileHistory>;
|
||||||
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
|
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
|
||||||
actionRunner: ActionRunner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => {
|
export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => {
|
||||||
|
@ -5,7 +5,6 @@ import { memo, useCallback, useEffect, useState, useMemo } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Popover, Transition } from '@headlessui/react';
|
import { Popover, Transition } from '@headlessui/react';
|
||||||
import { diffLines, type Change } from 'diff';
|
import { diffLines, type Change } from 'diff';
|
||||||
import { ActionRunner } from '~/lib/runtime/action-runner';
|
|
||||||
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
|
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
|
||||||
import type { FileHistory } from '~/types/actions';
|
import type { FileHistory } from '~/types/actions';
|
||||||
import { DiffView } from './DiffView';
|
import { DiffView } from './DiffView';
|
||||||
@ -32,7 +31,6 @@ import type { ElementInfo } from './Inspector';
|
|||||||
interface WorkspaceProps {
|
interface WorkspaceProps {
|
||||||
chatStarted?: boolean;
|
chatStarted?: boolean;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
actionRunner: ActionRunner;
|
|
||||||
metadata?: {
|
metadata?: {
|
||||||
gitUrl?: string;
|
gitUrl?: string;
|
||||||
};
|
};
|
||||||
@ -281,7 +279,7 @@ const FileModifiedDropdown = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Workbench = memo(
|
export const Workbench = memo(
|
||||||
({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
|
({ chatStarted, isStreaming, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
|
||||||
renderLogger.trace('Workbench');
|
renderLogger.trace('Workbench');
|
||||||
|
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
@ -486,7 +484,7 @@ export const Workbench = memo(
|
|||||||
initial={{ x: '100%' }}
|
initial={{ x: '100%' }}
|
||||||
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
|
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
|
||||||
>
|
>
|
||||||
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} actionRunner={actionRunner} />
|
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} />
|
||||||
</View>
|
</View>
|
||||||
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
|
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
|
||||||
<Preview setSelectedElement={setSelectedElement} />
|
<Preview setSelectedElement={setSelectedElement} />
|
||||||
|
@ -10,6 +10,7 @@ const logger = createScopedLogger('Terminal');
|
|||||||
|
|
||||||
export interface TerminalRef {
|
export interface TerminalRef {
|
||||||
reloadStyles: () => void;
|
reloadStyles: () => void;
|
||||||
|
getTerminal: () => XTerm | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalProps {
|
export interface TerminalProps {
|
||||||
@ -80,6 +81,9 @@ export const Terminal = memo(
|
|||||||
const terminal = terminalRef.current!;
|
const terminal = terminalRef.current!;
|
||||||
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
||||||
},
|
},
|
||||||
|
getTerminal: () => {
|
||||||
|
return terminalRef.current;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export const TerminalTabs = memo(() => {
|
|||||||
const terminalToggledByShortcut = useRef(false);
|
const terminalToggledByShortcut = useRef(false);
|
||||||
|
|
||||||
const [activeTerminal, setActiveTerminal] = useState(0);
|
const [activeTerminal, setActiveTerminal] = useState(0);
|
||||||
const [terminalCount, setTerminalCount] = useState(1);
|
const [terminalCount, setTerminalCount] = useState(0);
|
||||||
|
|
||||||
const addTerminal = () => {
|
const addTerminal = () => {
|
||||||
if (terminalCount < MAX_TERMINALS) {
|
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(() => {
|
useEffect(() => {
|
||||||
const { current: terminal } = terminalPanelRef;
|
const { current: terminal } = terminalPanelRef;
|
||||||
|
|
||||||
@ -125,6 +169,15 @@ export const TerminalTabs = memo(() => {
|
|||||||
>
|
>
|
||||||
<div className="i-ph:terminal-window-duotone text-lg" />
|
<div className="i-ph:terminal-window-duotone text-lg" />
|
||||||
Terminal {terminalCount > 1 && index}
|
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>
|
</button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
@ -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) {
|
onTerminalResize(cols: number, rows: number) {
|
||||||
for (const { process } of this.#terminals) {
|
for (const { process } of this.#terminals) {
|
||||||
process.resize({ cols, rows });
|
process.resize({ cols, rows });
|
||||||
|
@ -147,6 +147,10 @@ export class WorkbenchStore {
|
|||||||
this.#terminalStore.attachBoltTerminal(terminal);
|
this.#terminalStore.attachBoltTerminal(terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detachTerminal(terminal: ITerminal) {
|
||||||
|
this.#terminalStore.detachTerminal(terminal);
|
||||||
|
}
|
||||||
|
|
||||||
onTerminalResize(cols: number, rows: number) {
|
onTerminalResize(cols: number, rows: number) {
|
||||||
this.#terminalStore.onTerminalResize(cols, rows);
|
this.#terminalStore.onTerminalResize(cols, rows);
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,28 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const stream = new SwitchableStream();
|
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 = {
|
const cumulativeUsage = {
|
||||||
completionTokens: 0,
|
completionTokens: 0,
|
||||||
@ -298,6 +320,30 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
messageSliceId,
|
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 () => {
|
(async () => {
|
||||||
for await (const part of result.fullStream) {
|
for await (const part of result.fullStream) {
|
||||||
if (part.type === 'error') {
|
if (part.type === 'error') {
|
||||||
@ -310,7 +356,10 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
})();
|
})();
|
||||||
result.mergeIntoDataStream(dataStream);
|
result.mergeIntoDataStream(dataStream);
|
||||||
},
|
},
|
||||||
onError: (error: any) => `Custom error: ${error.message}`,
|
onError: (error: any) => {
|
||||||
|
cleanup();
|
||||||
|
return `Custom error: ${error.message}`;
|
||||||
|
},
|
||||||
}).pipeThrough(
|
}).pipeThrough(
|
||||||
new TransformStream({
|
new TransformStream({
|
||||||
transform: (chunk, controller) => {
|
transform: (chunk, controller) => {
|
||||||
@ -359,6 +408,9 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
// Ensure cleanup on error
|
||||||
|
cleanup();
|
||||||
|
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
if (error.message?.includes('API key')) {
|
if (error.message?.includes('API key')) {
|
||||||
|
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,9 @@ import type { Template } from '~/types/template';
|
|||||||
import { STARTER_TEMPLATES } from './constants';
|
import { STARTER_TEMPLATES } from './constants';
|
||||||
|
|
||||||
const starterTemplateSelectionPrompt = (templates: Template[]) => `
|
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:
|
Available templates:
|
||||||
<template>
|
<template>
|
||||||
|
Loading…
Reference in New Issue
Block a user