Add persistent file locking feature with enhanced UI

This commit is contained in:
Stijnus 2025-05-02 17:14:07 +02:00
parent 844da4b1c2
commit 4a3009837a
11 changed files with 640 additions and 12 deletions

View File

@ -1,6 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion';
import type { ActionAlert } from '~/types/actions';
import { classNames } from '~/utils/classNames';
import LockAlert from './LockAlert';
interface Props {
alert: ActionAlert;
@ -9,7 +10,12 @@ interface Props {
}
export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
const { description, content, source } = alert;
const { description, content, source, isLockedFile } = alert;
/* If this is a locked file alert, use the dedicated LockAlert component */
if (isLockedFile) {
return <LockAlert alert={alert} clearAlert={clearAlert} />;
}
const isPreview = source === 'preview';
const title = isPreview ? 'Preview Error' : 'Terminal Error';

View File

@ -0,0 +1,92 @@
import { AnimatePresence, motion } from 'framer-motion';
import type { ActionAlert } from '~/types/actions';
import { classNames } from '~/utils/classNames';
interface Props {
alert: ActionAlert;
clearAlert: () => void;
}
export default function LockAlert({ alert, clearAlert }: Props) {
const { description } = alert;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="rounded-lg border border-bolt-elements-borderColor border-l-2 border-l-amber-500 bg-bolt-elements-background-depth-2 mb-2 overflow-hidden"
>
{/* Header */}
<div className="p-4 pb-2 flex items-center">
<motion.div
className="flex-shrink-0 mr-2"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
<div className="i-ph:lock-simple-duotone text-xl text-amber-500"></div>
</motion.div>
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-sm font-medium text-amber-500"
>
Terminal Error
</motion.h3>
</div>
{/* Content */}
<div className="px-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="text-sm text-bolt-elements-textSecondary"
>
<p className="mb-3">
The file is locked and cannot be modified. You need to unlock the file before making changes.
</p>
{description && (
<div className="text-xs p-3 bg-bolt-elements-background-depth-3 rounded-md mb-3 font-mono border border-bolt-elements-borderColor">
<div className="flex items-center mb-1">
<div className="i-ph:lock-simple-duotone text-sm text-amber-500 mr-1.5"></div>
<span className="font-medium text-bolt-elements-textPrimary">Error Details</span>
</div>
<div className="pl-5 border-l border-bolt-elements-borderColor">{description}</div>
</div>
)}
</motion.div>
</div>
{/* Actions */}
<motion.div
className="p-3 bg-bolt-elements-background-depth-1 border-t border-bolt-elements-borderColor"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex justify-end gap-2">
<button
onClick={clearAlert}
className={classNames(
`px-3 py-2 rounded-md text-sm font-medium transition-colors`,
'bg-bolt-elements-button-secondary-background',
'hover:bg-bolt-elements-button-secondary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
'text-bolt-elements-button-secondary-text',
'hover:bg-amber-50 dark:hover:bg-amber-950/20',
)}
>
Dismiss
</button>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}

View File

@ -21,6 +21,7 @@ import type { Theme } from '~/types/theme';
import { classNames } from '~/utils/classNames';
import { debounce } from '~/utils/debounce';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { isFileLocked } from '~/utils/fileLocks';
import { BinaryContent } from './BinaryContent';
import { getTheme, reconfigureTheme } from './cm-theme';
import { indentKeyBinding } from './indent';
@ -29,6 +30,9 @@ import { createEnvMaskingExtension } from './EnvMasking';
const logger = createScopedLogger('CodeMirrorEditor');
// Create a module-level reference to the current document for use in tooltip functions
let currentDocRef: EditorDocument | undefined;
export interface EditorDocument {
value: string;
isBinary: boolean;
@ -158,6 +162,9 @@ export const CodeMirrorEditor = memo(
onChangeRef.current = onChange;
onSaveRef.current = onSave;
docRef.current = doc;
// Update the module-level reference for use in tooltip functions
currentDocRef = doc;
themeRef.current = theme;
});
@ -278,6 +285,15 @@ export const CodeMirrorEditor = memo(
autoFocusOnDocumentChange,
doc as TextEditorDocument,
);
// Check if the file is locked and update the editor state accordingly
const { locked } = isFileLocked(doc.filePath);
if (locked) {
view.dispatch({
effects: [editableStateEffect.of(false)],
});
}
}, [doc?.value, editable, doc?.filePath, autoFocusOnDocumentChange]);
return (
@ -419,8 +435,12 @@ function setEditorDocument(
});
}
// Check if the file is locked
const { locked } = isFileLocked(doc.filePath);
// Set editable state based on both the editable prop and the file's lock state
view.dispatch({
effects: [editableStateEffect.of(editable && !doc.isBinary)],
effects: [editableStateEffect.of(editable && !doc.isBinary && !locked)],
});
getLanguage(doc.filePath).then((languageSupport) => {
@ -477,6 +497,22 @@ function getReadOnlyTooltip(state: EditorState) {
return [];
}
// Get the current document from the module-level reference
const currentDoc = currentDocRef;
let tooltipMessage = 'Cannot edit file while AI response is being generated';
// If we have a current document, check if it's locked
if (currentDoc?.filePath) {
const { locked, lockMode } = isFileLocked(currentDoc.filePath);
if (locked) {
tooltipMessage =
lockMode === 'full'
? 'This file is locked and cannot be edited'
: 'This file has scoped locking - only additions are allowed';
}
}
return state.selection.ranges
.filter((range) => {
return range.empty;
@ -490,7 +526,7 @@ function getReadOnlyTooltip(state: EditorState) {
create: () => {
const divElement = document.createElement('div');
divElement.className = 'cm-readonly-tooltip';
divElement.textContent = 'Cannot edit file while AI response is being generated';
divElement.textContent = tooltipMessage;
return { dom: divElement };
},

View File

@ -71,7 +71,12 @@ export const EditorPanel = memo(
}, [editorDocument]);
const activeFileUnsaved = useMemo(() => {
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
if (!editorDocument || !unsavedFiles) {
return false;
}
// Make sure unsavedFiles is a Set before calling has()
return unsavedFiles instanceof Set && unsavedFiles.has(editorDocument.filePath);
}, [editorDocument, unsavedFiles]);
return (

View File

@ -152,7 +152,7 @@ export const FileTree = memo(
key={fileOrFolder.id}
selected={selectedFile === fileOrFolder.fullPath}
file={fileOrFolder}
unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
unsavedChanges={unsavedFiles instanceof Set && unsavedFiles.has(fileOrFolder.fullPath)}
fileHistory={fileHistory}
onCopyPath={() => {
onCopyPath(fileOrFolder);
@ -402,6 +402,66 @@ function FileContextMenu({
}
};
// Handler for locking a file with full lock
const handleLockFile = () => {
try {
if (isFolder) {
return;
}
const success = workbenchStore.lockFile(fullPath, 'full');
if (success) {
toast.success(`File locked successfully`);
} else {
toast.error(`Failed to lock file`);
}
} catch (error) {
toast.error(`Error locking file`);
logger.error(error);
}
};
// Handler for locking a file with scoped lock
const handleScopedLockFile = () => {
try {
if (isFolder) {
return;
}
const success = workbenchStore.lockFile(fullPath, 'scoped');
if (success) {
toast.success(`File locked (scoped) successfully`);
} else {
toast.error(`Failed to lock file`);
}
} catch (error) {
toast.error(`Error locking file`);
logger.error(error);
}
};
// Handler for unlocking a file
const handleUnlockFile = () => {
try {
if (isFolder) {
return;
}
const success = workbenchStore.unlockFile(fullPath);
if (success) {
toast.success(`File unlocked successfully`);
} else {
toast.error(`Failed to unlock file`);
}
} catch (error) {
toast.error(`Error unlocking file`);
logger.error(error);
}
};
return (
<>
<ContextMenu.Root>
@ -441,6 +501,29 @@ function FileContextMenu({
<ContextMenuItem onSelect={onCopyPath}>Copy path</ContextMenuItem>
<ContextMenuItem onSelect={onCopyRelativePath}>Copy relative path</ContextMenuItem>
</ContextMenu.Group>
{/* Add lock/unlock options for files only */}
{!isFolder && (
<ContextMenu.Group className="p-1 border-t-px border-solid border-bolt-elements-borderColor">
<ContextMenuItem onSelect={handleLockFile}>
<div className="flex items-center gap-2">
<div className="i-ph:lock-simple" />
Lock File (Full)
</div>
</ContextMenuItem>
<ContextMenuItem onSelect={handleScopedLockFile}>
<div className="flex items-center gap-2">
<div className="i-ph:lock-simple-open" />
Lock File (Scoped)
</div>
</ContextMenuItem>
<ContextMenuItem onSelect={handleUnlockFile}>
<div className="flex items-center gap-2">
<div className="i-ph:lock-key-open" />
Unlock File
</div>
</ContextMenuItem>
</ContextMenu.Group>
)}
{/* Add delete option in a new group */}
<ContextMenu.Group className="p-1 border-t-px border-solid border-bolt-elements-borderColor">
<ContextMenuItem onSelect={handleDelete}>
@ -516,6 +599,9 @@ function File({
}: FileProps) {
const { depth, name, fullPath } = file;
// Check if the file is locked
const { locked, lockMode } = workbenchStore.isFileLocked(fullPath);
const fileModifications = fileHistory[fullPath];
const { additions, deletions } = useMemo(() => {
@ -582,6 +668,17 @@ function File({
{deletions > 0 && <span className="text-red-500">-{deletions}</span>}
</div>
)}
{locked && (
<span
className={classNames(
'shrink-0',
lockMode === 'full'
? 'i-ph:lock-simple scale-80 text-red-500'
: 'i-ph:lock-simple-open scale-80 text-yellow-500',
)}
title={lockMode === 'full' ? 'File is fully locked' : 'File has scoped locking'}
/>
)}
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
</div>
</div>

View File

@ -0,0 +1,89 @@
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('LockedFiles');
// Key for storing locked files in localStorage
export const LOCKED_FILES_KEY = 'bolt.lockedFiles';
export type LockMode = 'full' | 'scoped';
export interface LockedFile {
path: string;
lockMode: LockMode;
}
/**
* Save locked files to localStorage
*/
export function saveLockedFiles(files: LockedFile[]): void {
try {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(LOCKED_FILES_KEY, JSON.stringify(files));
}
} catch (error) {
logger.error('Failed to save locked files to localStorage', error);
}
}
/**
* Get locked files from localStorage
*/
export function getLockedFiles(): LockedFile[] {
try {
if (typeof localStorage !== 'undefined') {
const lockedFilesJson = localStorage.getItem(LOCKED_FILES_KEY);
if (lockedFilesJson) {
return JSON.parse(lockedFilesJson);
}
}
return [];
} catch (error) {
logger.error('Failed to get locked files from localStorage', error);
return [];
}
}
/**
* Add a file to the locked files list
*/
export function addLockedFile(filePath: string, lockMode: LockMode): void {
const lockedFiles = getLockedFiles();
// Remove any existing entry for this file
const filteredFiles = lockedFiles.filter((file) => file.path !== filePath);
// Add the new entry
filteredFiles.push({ path: filePath, lockMode });
// Save the updated list
saveLockedFiles(filteredFiles);
}
/**
* Remove a file from the locked files list
*/
export function removeLockedFile(filePath: string): void {
const lockedFiles = getLockedFiles();
// Filter out the file to remove
const filteredFiles = lockedFiles.filter((file) => file.path !== filePath);
// Save the updated list
saveLockedFiles(filteredFiles);
}
/**
* Check if a file is locked
*/
export function isFileLocked(filePath: string): { locked: boolean; lockMode?: LockMode } {
const lockedFiles = getLockedFiles();
const lockedFile = lockedFiles.find((file) => file.path === filePath);
if (lockedFile) {
return { locked: true, lockMode: lockedFile.lockMode };
}
return { locked: false };
}

View File

@ -1,11 +1,14 @@
import { atom, computed, map, type MapStore, type WritableAtom } from 'nanostores';
import type { EditorDocument, ScrollPosition } from '~/components/editor/codemirror/CodeMirrorEditor';
import type { FileMap, FilesStore } from './files';
import { createScopedLogger } from '~/utils/logger';
export type EditorDocuments = Record<string, EditorDocument>;
type SelectedFile = WritableAtom<string | undefined>;
const logger = createScopedLogger('EditorStore');
export class EditorStore {
#filesStore: FilesStore;
@ -82,6 +85,20 @@ export class EditorStore {
return;
}
// Check if the file is locked by getting the file from the filesStore
const file = this.#filesStore.getFile(filePath);
if (file?.locked && file.lockMode === 'full') {
logger.warn(`Attempted to update locked file: ${filePath}`);
return;
}
/*
* For scoped locks, we would need to implement diff checking here
* to determine if the edit is modifying existing code or just adding new code
* This is a more complex feature that would be implemented in a future update
*/
const currentContent = documentState.value;
const contentChanged = currentContent !== newContent;

View File

@ -8,6 +8,7 @@ import { WORK_DIR } from '~/utils/constants';
import { computeFileModifications } from '~/utils/diff';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
import { getLockedFiles, addLockedFile, removeLockedFile, type LockMode } from '~/lib/persistence/lockedFiles';
const logger = createScopedLogger('FilesStore');
@ -17,6 +18,8 @@ export interface File {
type: 'file';
content: string;
isBinary: boolean;
locked?: boolean;
lockMode?: 'full' | 'scoped' | null;
}
export interface Folder {
@ -76,6 +79,9 @@ export class FilesStore {
logger.error('Failed to load deleted paths from localStorage', error);
}
// Load locked files from localStorage
this.#loadLockedFiles();
if (import.meta.hot) {
// Persist our state across hot reloads
import.meta.hot.data.files = this.files;
@ -86,6 +92,121 @@ export class FilesStore {
this.#init();
}
/**
* Load locked files from localStorage and update the file objects
*/
#loadLockedFiles() {
try {
const lockedFiles = getLockedFiles();
if (lockedFiles.length === 0) {
logger.info('No locked files found in localStorage');
return;
}
logger.info(`Found ${lockedFiles.length} locked files in localStorage`);
const currentFiles = this.files.get();
const updates: FileMap = {};
for (const lockedFile of lockedFiles) {
logger.info(`Applying lock to file: ${lockedFile.path} (mode: ${lockedFile.lockMode})`);
const file = currentFiles[lockedFile.path];
if (file?.type === 'file') {
updates[lockedFile.path] = {
...file,
locked: true,
lockMode: lockedFile.lockMode,
};
} else {
/*
* The file exists in localStorage but not in the current files
* This could happen if the file was deleted or renamed
* We'll keep track of it anyway in case it gets created later
*/
logger.warn(`Locked file not found in current files: ${lockedFile.path}`);
}
}
if (Object.keys(updates).length > 0) {
logger.info(`Updating ${Object.keys(updates).length} files with lock state`);
this.files.set({ ...currentFiles, ...updates });
}
} catch (error) {
logger.error('Failed to load locked files from localStorage', error);
}
}
/**
* Lock a file
*/
lockFile(filePath: string, lockMode: LockMode) {
const file = this.getFile(filePath);
if (!file) {
logger.error(`Cannot lock non-existent file: ${filePath}`);
return false;
}
// Update the file in the store
this.files.setKey(filePath, {
...file,
locked: true,
lockMode,
});
// Persist to localStorage
addLockedFile(filePath, lockMode);
logger.info(`File locked: ${filePath} (mode: ${lockMode})`);
return true;
}
/**
* Unlock a file
*/
unlockFile(filePath: string) {
const file = this.getFile(filePath);
if (!file) {
logger.error(`Cannot unlock non-existent file: ${filePath}`);
return false;
}
// Update the file in the store
this.files.setKey(filePath, {
...file,
locked: false,
lockMode: null,
});
// Remove from localStorage
removeLockedFile(filePath);
logger.info(`File unlocked: ${filePath}`);
return true;
}
/**
* Check if a file is locked
*/
isFileLocked(filePath: string): { locked: boolean; lockMode?: LockMode } {
const file = this.getFile(filePath);
if (!file) {
return { locked: false };
}
return {
locked: !!file.locked,
lockMode: file.lockMode as LockMode | undefined,
};
}
getFile(filePath: string) {
const dirent = this.files.get()[filePath];
@ -149,8 +270,19 @@ export class FilesStore {
this.#modifiedFiles.set(filePath, oldContent);
}
// Get the current lock state before updating
const currentFile = this.files.get()[filePath];
const locked = currentFile?.type === 'file' ? currentFile.locked : false;
const lockMode = currentFile?.type === 'file' ? currentFile.lockMode : null;
// we immediately update the file and don't rely on the `change` event coming from the watcher
this.files.setKey(filePath, { type: 'file', content, isBinary: false });
this.files.setKey(filePath, {
type: 'file',
content,
isBinary: false,
locked,
lockMode,
});
logger.info('File updated');
} catch (error) {
@ -166,10 +298,28 @@ export class FilesStore {
// Clean up any files that were previously deleted
this.#cleanupDeletedFiles();
// Set up file watcher
webcontainer.internal.watchPaths(
{ include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true },
bufferWatchEvents(100, this.#processEventBuffer.bind(this)),
);
// Load locked files immediately
this.#loadLockedFiles();
/*
* Also set up a timer to load locked files again after a delay
* This ensures that locks are applied even if files are loaded asynchronously
*/
setTimeout(() => {
this.#loadLockedFiles();
logger.info('Reapplied locked files from localStorage');
}, 2000);
// Set up a periodic check to ensure locks remain applied
setInterval(() => {
this.#loadLockedFiles();
}, 10000);
}
/**
@ -302,7 +452,17 @@ export class FilesStore {
content = existingFile.content;
}
this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
// Preserve lock state if the file already exists
const locked = existingFile?.type === 'file' ? existingFile.locked : false;
const lockMode = existingFile?.type === 'file' ? existingFile.lockMode : null;
this.files.setKey(sanitizedPath, {
type: 'file',
content,
isBinary,
locked,
lockMode,
});
break;
}
case 'remove_file': {
@ -353,14 +513,26 @@ export class FilesStore {
await webcontainer.fs.writeFile(relativePath, Buffer.from(content));
const base64Content = Buffer.from(content).toString('base64');
this.files.setKey(filePath, { type: 'file', content: base64Content, isBinary: true });
this.files.setKey(filePath, {
type: 'file',
content: base64Content,
isBinary: true,
locked: false,
lockMode: null,
});
this.#modifiedFiles.set(filePath, base64Content);
} else {
const contentToWrite = (content as string).length === 0 ? ' ' : content;
await webcontainer.fs.writeFile(relativePath, contentToWrite);
this.files.setKey(filePath, { type: 'file', content: content as string, isBinary: false });
this.files.setKey(filePath, {
type: 'file',
content: content as string,
isBinary: false,
locked: false,
lockMode: null,
});
this.#modifiedFiles.set(filePath, content as string);
}

View File

@ -49,11 +49,11 @@ export class WorkbenchStore {
currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
actionAlert: WritableAtom<ActionAlert | undefined> =
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
import.meta.hot?.data.actionAlert ?? atom<ActionAlert | undefined>(undefined);
supabaseAlert: WritableAtom<SupabaseAlert | undefined> =
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
import.meta.hot?.data.supabaseAlert ?? atom<SupabaseAlert | undefined>(undefined);
deployAlert: WritableAtom<DeployAlert | undefined> =
import.meta.hot?.data.unsavedFiles ?? atom<DeployAlert | undefined>(undefined);
import.meta.hot?.data.deployAlert ?? atom<DeployAlert | undefined>(undefined);
modifiedFiles = new Set<string>();
artifactIdList: string[] = [];
#globalExecutionQueue = Promise.resolve();
@ -226,6 +226,30 @@ export class WorkbenchStore {
return;
}
// Check if the file is locked
const { locked, lockMode } = this.isFileLocked(filePath);
if (locked && lockMode === 'full') {
// File is fully locked, don't allow saving
console.warn(`Attempted to save locked file: ${filePath}`);
this.actionAlert.set({
type: 'error',
title: 'File Save Blocked',
description: `Cannot save locked file: ${filePath}`,
content: 'This file is locked and cannot be modified.',
isLockedFile: true,
});
return;
}
/*
* For scoped locks, we would need to implement diff checking here
* to determine if the user is modifying existing code or just adding new code
* This is a more complex feature that would be implemented in a future update
*/
await this.#filesStore.saveFile(filePath, document.value);
const newUnsavedFiles = new Set(this.unsavedFiles.get());
@ -279,6 +303,34 @@ export class WorkbenchStore {
this.#filesStore.resetFileModifications();
}
/**
* Lock a file to prevent edits
* @param filePath Path to the file to lock
* @param lockMode Type of lock to apply ("full" or "scoped")
* @returns True if the file was successfully locked
*/
lockFile(filePath: string, lockMode: 'full' | 'scoped') {
return this.#filesStore.lockFile(filePath, lockMode);
}
/**
* Unlock a file to allow edits
* @param filePath Path to the file to unlock
* @returns True if the file was successfully unlocked
*/
unlockFile(filePath: string) {
return this.#filesStore.unlockFile(filePath);
}
/**
* Check if a file is locked
* @param filePath Path to the file to check
* @returns Object with locked status and lock mode
*/
isFileLocked(filePath: string) {
return this.#filesStore.isFileLocked(filePath);
}
async createFile(filePath: string, content: string | Uint8Array = '') {
try {
const success = await this.#filesStore.createFile(filePath, content);
@ -497,6 +549,43 @@ export class WorkbenchStore {
const wc = await webcontainer;
const fullPath = path.join(wc.workdir, data.action.filePath);
// Check if the file is locked
const { locked, lockMode } = this.isFileLocked(fullPath);
if (locked && lockMode === 'full') {
// File is fully locked, don't allow any modifications
console.warn(`AI attempted to modify locked file: ${fullPath}`);
/*
* Instead of trying to update the action directly, we'll just add a notification
* and prevent the action from being executed
*/
this.actionAlert.set({
type: 'error',
title: 'File Modification Blocked',
description: `AI attempted to modify locked file: ${data.action.filePath}`,
content: 'This file is locked and cannot be modified by the AI.',
isLockedFile: true,
});
// Still select the file to show the user what the AI tried to modify
if (this.selectedFile.value !== fullPath) {
this.setSelectedFile(fullPath);
}
if (this.currentView.value !== 'code') {
this.currentView.set('code');
}
return;
}
/*
* For scoped locks, we would need to implement diff checking here
* to determine if the AI is modifying existing code or just adding new code
* This is a more complex feature that would be implemented in a future update
*/
if (this.selectedFile.value !== fullPath) {
this.setSelectedFile(fullPath);
}

View File

@ -40,6 +40,7 @@ export interface ActionAlert {
description: string;
content: string;
source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors
isLockedFile?: boolean; // Indicates if the error is related to a locked file
}
export interface SupabaseAlert {

24
app/utils/fileLocks.ts Normal file
View File

@ -0,0 +1,24 @@
import { getLockedFiles, type LockMode } from '~/lib/persistence/lockedFiles';
import { createScopedLogger } from './logger';
const logger = createScopedLogger('FileLocks');
/**
* Check if a file is locked directly from localStorage
* This avoids circular dependencies between components and stores
*/
export function isFileLocked(filePath: string): { locked: boolean; lockMode?: LockMode } {
try {
const lockedFiles = getLockedFiles();
const lockedFile = lockedFiles.find((file) => file.path === filePath);
if (lockedFile) {
return { locked: true, lockMode: lockedFile.lockMode };
}
return { locked: false };
} catch (error) {
logger.error('Failed to check if file is locked', error);
return { locked: false };
}
}