mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Fix file locking to be scoped by chat ID
This commit is contained in:
parent
4a3009837a
commit
f447946037
@ -21,7 +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 { isFileLocked, getCurrentChatId } from '~/utils/fileLocks';
|
||||
import { BinaryContent } from './BinaryContent';
|
||||
import { getTheme, reconfigureTheme } from './cm-theme';
|
||||
import { indentKeyBinding } from './indent';
|
||||
@ -287,7 +287,8 @@ export const CodeMirrorEditor = memo(
|
||||
);
|
||||
|
||||
// Check if the file is locked and update the editor state accordingly
|
||||
const { locked } = isFileLocked(doc.filePath);
|
||||
const currentChatId = getCurrentChatId();
|
||||
const { locked } = isFileLocked(doc.filePath, currentChatId);
|
||||
|
||||
if (locked) {
|
||||
view.dispatch({
|
||||
@ -436,7 +437,8 @@ function setEditorDocument(
|
||||
}
|
||||
|
||||
// Check if the file is locked
|
||||
const { locked } = isFileLocked(doc.filePath);
|
||||
const currentChatId = getCurrentChatId();
|
||||
const { locked } = isFileLocked(doc.filePath, currentChatId);
|
||||
|
||||
// Set editable state based on both the editable prop and the file's lock state
|
||||
view.dispatch({
|
||||
@ -503,7 +505,8 @@ function getReadOnlyTooltip(state: EditorState) {
|
||||
|
||||
// If we have a current document, check if it's locked
|
||||
if (currentDoc?.filePath) {
|
||||
const { locked, lockMode } = isFileLocked(currentDoc.filePath);
|
||||
const currentChatId = getCurrentChatId();
|
||||
const { locked, lockMode } = isFileLocked(currentDoc.filePath, currentChatId);
|
||||
|
||||
if (locked) {
|
||||
tooltipMessage =
|
||||
|
@ -8,6 +8,7 @@ export const LOCKED_FILES_KEY = 'bolt.lockedFiles';
|
||||
export type LockMode = 'full' | 'scoped';
|
||||
|
||||
export interface LockedFile {
|
||||
chatId: string; // Chat ID to scope locks to a specific project
|
||||
path: string;
|
||||
lockMode: LockMode;
|
||||
}
|
||||
@ -47,15 +48,18 @@ export function getLockedFiles(): LockedFile[] {
|
||||
|
||||
/**
|
||||
* Add a file to the locked files list
|
||||
* @param chatId The chat ID to scope the lock to
|
||||
* @param filePath The path of the file to lock
|
||||
* @param lockMode The type of lock to apply
|
||||
*/
|
||||
export function addLockedFile(filePath: string, lockMode: LockMode): void {
|
||||
export function addLockedFile(chatId: string, filePath: string, lockMode: LockMode): void {
|
||||
const lockedFiles = getLockedFiles();
|
||||
|
||||
// Remove any existing entry for this file
|
||||
const filteredFiles = lockedFiles.filter((file) => file.path !== filePath);
|
||||
// Remove any existing entry for this file in this chat
|
||||
const filteredFiles = lockedFiles.filter((file) => !(file.chatId === chatId && file.path === filePath));
|
||||
|
||||
// Add the new entry
|
||||
filteredFiles.push({ path: filePath, lockMode });
|
||||
filteredFiles.push({ chatId, path: filePath, lockMode });
|
||||
|
||||
// Save the updated list
|
||||
saveLockedFiles(filteredFiles);
|
||||
@ -63,12 +67,14 @@ export function addLockedFile(filePath: string, lockMode: LockMode): void {
|
||||
|
||||
/**
|
||||
* Remove a file from the locked files list
|
||||
* @param chatId The chat ID the lock belongs to
|
||||
* @param filePath The path of the file to unlock
|
||||
*/
|
||||
export function removeLockedFile(filePath: string): void {
|
||||
export function removeLockedFile(chatId: string, filePath: string): void {
|
||||
const lockedFiles = getLockedFiles();
|
||||
|
||||
// Filter out the file to remove
|
||||
const filteredFiles = lockedFiles.filter((file) => file.path !== filePath);
|
||||
// Filter out the file to remove for this specific chat
|
||||
const filteredFiles = lockedFiles.filter((file) => !(file.chatId === chatId && file.path === filePath));
|
||||
|
||||
// Save the updated list
|
||||
saveLockedFiles(filteredFiles);
|
||||
@ -76,10 +82,13 @@ export function removeLockedFile(filePath: string): void {
|
||||
|
||||
/**
|
||||
* Check if a file is locked
|
||||
* @param chatId The chat ID to check locks for
|
||||
* @param filePath The path of the file to check
|
||||
* @returns Object with locked status and lock mode
|
||||
*/
|
||||
export function isFileLocked(filePath: string): { locked: boolean; lockMode?: LockMode } {
|
||||
export function isFileLocked(chatId: string, filePath: string): { locked: boolean; lockMode?: LockMode } {
|
||||
const lockedFiles = getLockedFiles();
|
||||
const lockedFile = lockedFiles.find((file) => file.path === filePath);
|
||||
const lockedFile = lockedFiles.find((file) => file.chatId === chatId && file.path === filePath);
|
||||
|
||||
if (lockedFile) {
|
||||
return { locked: true, lockMode: lockedFile.lockMode };
|
||||
@ -87,3 +96,51 @@ export function isFileLocked(filePath: string): { locked: boolean; lockMode?: Lo
|
||||
|
||||
return { locked: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all locked files for a specific chat
|
||||
* @param chatId The chat ID to get locks for
|
||||
* @returns Array of locked files for the specified chat
|
||||
*/
|
||||
export function getLockedFilesForChat(chatId: string): LockedFile[] {
|
||||
const lockedFiles = getLockedFiles();
|
||||
return lockedFiles.filter((file) => file.chatId === chatId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate legacy locks (without chatId) to the new format
|
||||
* @param currentChatId The current chat ID to assign to legacy locks
|
||||
*/
|
||||
export function migrateLegacyLocks(currentChatId: string): void {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const lockedFilesJson = localStorage.getItem(LOCKED_FILES_KEY);
|
||||
|
||||
if (lockedFilesJson) {
|
||||
const lockedFiles = JSON.parse(lockedFilesJson);
|
||||
|
||||
if (Array.isArray(lockedFiles)) {
|
||||
let hasLegacyLocks = false;
|
||||
|
||||
// Check if any locks are in the old format (missing chatId)
|
||||
const updatedLocks = lockedFiles.map((file) => {
|
||||
if (!file.chatId) {
|
||||
hasLegacyLocks = true;
|
||||
return { ...file, chatId: currentChatId };
|
||||
}
|
||||
|
||||
return file;
|
||||
});
|
||||
|
||||
// Only save if we found and updated legacy locks
|
||||
if (hasLegacyLocks) {
|
||||
saveLockedFiles(updatedLocks);
|
||||
logger.info(`Migrated ${updatedLocks.length} legacy locks to chat ID: ${currentChatId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to migrate legacy locks', error);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,14 @@ 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';
|
||||
import {
|
||||
addLockedFile,
|
||||
removeLockedFile,
|
||||
getLockedFilesForChat,
|
||||
migrateLegacyLocks,
|
||||
type LockMode,
|
||||
} from '~/lib/persistence/lockedFiles';
|
||||
import { getCurrentChatId } from '~/utils/fileLocks';
|
||||
|
||||
const logger = createScopedLogger('FilesStore');
|
||||
|
||||
@ -89,22 +96,47 @@ export class FilesStore {
|
||||
import.meta.hot.data.deletedPaths = this.#deletedPaths;
|
||||
}
|
||||
|
||||
// Listen for URL changes to detect chat ID changes
|
||||
if (typeof window !== 'undefined') {
|
||||
let lastChatId = getCurrentChatId();
|
||||
|
||||
// Use MutationObserver to detect URL changes (for SPA navigation)
|
||||
const observer = new MutationObserver(() => {
|
||||
const currentChatId = getCurrentChatId();
|
||||
|
||||
if (currentChatId !== lastChatId) {
|
||||
logger.info(`Chat ID changed from ${lastChatId} to ${currentChatId}, reloading locks`);
|
||||
lastChatId = currentChatId;
|
||||
this.#loadLockedFiles(currentChatId);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document, { subtree: true, childList: true });
|
||||
}
|
||||
|
||||
this.#init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load locked files from localStorage and update the file objects
|
||||
* @param chatId Optional chat ID to load locks for (defaults to current chat)
|
||||
*/
|
||||
#loadLockedFiles() {
|
||||
#loadLockedFiles(chatId?: string) {
|
||||
try {
|
||||
const lockedFiles = getLockedFiles();
|
||||
const currentChatId = chatId || getCurrentChatId();
|
||||
|
||||
// Migrate any legacy locks to the current chat
|
||||
migrateLegacyLocks(currentChatId);
|
||||
|
||||
// Get locks for the current chat
|
||||
const lockedFiles = getLockedFilesForChat(currentChatId);
|
||||
|
||||
if (lockedFiles.length === 0) {
|
||||
logger.info('No locked files found in localStorage');
|
||||
logger.info(`No locked files found for chat ID: ${currentChatId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Found ${lockedFiles.length} locked files in localStorage`);
|
||||
logger.info(`Found ${lockedFiles.length} locked files for chat ID: ${currentChatId}`);
|
||||
|
||||
const currentFiles = this.files.get();
|
||||
const updates: FileMap = {};
|
||||
@ -141,9 +173,14 @@ export class FilesStore {
|
||||
|
||||
/**
|
||||
* Lock a file
|
||||
* @param filePath Path to the file to lock
|
||||
* @param lockMode Type of lock to apply
|
||||
* @param chatId Optional chat ID (defaults to current chat)
|
||||
* @returns True if the file was successfully locked
|
||||
*/
|
||||
lockFile(filePath: string, lockMode: LockMode) {
|
||||
lockFile(filePath: string, lockMode: LockMode, chatId?: string) {
|
||||
const file = this.getFile(filePath);
|
||||
const currentChatId = chatId || getCurrentChatId();
|
||||
|
||||
if (!file) {
|
||||
logger.error(`Cannot lock non-existent file: ${filePath}`);
|
||||
@ -157,19 +194,23 @@ export class FilesStore {
|
||||
lockMode,
|
||||
});
|
||||
|
||||
// Persist to localStorage
|
||||
addLockedFile(filePath, lockMode);
|
||||
// Persist to localStorage with chat ID
|
||||
addLockedFile(currentChatId, filePath, lockMode);
|
||||
|
||||
logger.info(`File locked: ${filePath} (mode: ${lockMode})`);
|
||||
logger.info(`File locked: ${filePath} (mode: ${lockMode}) for chat: ${currentChatId}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a file
|
||||
* @param filePath Path to the file to unlock
|
||||
* @param chatId Optional chat ID (defaults to current chat)
|
||||
* @returns True if the file was successfully unlocked
|
||||
*/
|
||||
unlockFile(filePath: string) {
|
||||
unlockFile(filePath: string, chatId?: string) {
|
||||
const file = this.getFile(filePath);
|
||||
const currentChatId = chatId || getCurrentChatId();
|
||||
|
||||
if (!file) {
|
||||
logger.error(`Cannot unlock non-existent file: ${filePath}`);
|
||||
@ -183,28 +224,55 @@ export class FilesStore {
|
||||
lockMode: null,
|
||||
});
|
||||
|
||||
// Remove from localStorage
|
||||
removeLockedFile(filePath);
|
||||
// Remove from localStorage with chat ID
|
||||
removeLockedFile(currentChatId, filePath);
|
||||
|
||||
logger.info(`File unlocked: ${filePath}`);
|
||||
logger.info(`File unlocked: ${filePath} for chat: ${currentChatId}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is locked
|
||||
* @param filePath Path to the file to check
|
||||
* @param chatId Optional chat ID (defaults to current chat)
|
||||
* @returns Object with locked status and lock mode
|
||||
*/
|
||||
isFileLocked(filePath: string): { locked: boolean; lockMode?: LockMode } {
|
||||
isFileLocked(filePath: string, chatId?: string): { locked: boolean; lockMode?: LockMode } {
|
||||
const file = this.getFile(filePath);
|
||||
const currentChatId = chatId || getCurrentChatId();
|
||||
|
||||
if (!file) {
|
||||
return { locked: false };
|
||||
}
|
||||
|
||||
return {
|
||||
locked: !!file.locked,
|
||||
lockMode: file.lockMode as LockMode | undefined,
|
||||
};
|
||||
// First check the in-memory state
|
||||
if (file.locked) {
|
||||
return {
|
||||
locked: true,
|
||||
lockMode: file.lockMode as LockMode | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Then check localStorage for this specific chat
|
||||
* This ensures we catch any locks that might have been set in other tabs
|
||||
*/
|
||||
const lockedFiles = getLockedFilesForChat(currentChatId);
|
||||
const lockedFile = lockedFiles.find((file) => file.path === filePath);
|
||||
|
||||
if (lockedFile) {
|
||||
// Update the in-memory state to match localStorage
|
||||
this.files.setKey(filePath, {
|
||||
...file,
|
||||
locked: true,
|
||||
lockMode: lockedFile.lockMode,
|
||||
});
|
||||
|
||||
return { locked: true, lockMode: lockedFile.lockMode };
|
||||
}
|
||||
|
||||
return { locked: false };
|
||||
}
|
||||
|
||||
getFile(filePath: string) {
|
||||
@ -304,21 +372,28 @@ export class FilesStore {
|
||||
bufferWatchEvents(100, this.#processEventBuffer.bind(this)),
|
||||
);
|
||||
|
||||
// Load locked files immediately
|
||||
this.#loadLockedFiles();
|
||||
// Get the current chat ID
|
||||
const currentChatId = getCurrentChatId();
|
||||
|
||||
// Migrate any legacy locks to the current chat
|
||||
migrateLegacyLocks(currentChatId);
|
||||
|
||||
// Load locked files immediately for the current chat
|
||||
this.#loadLockedFiles(currentChatId);
|
||||
|
||||
/*
|
||||
* 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');
|
||||
this.#loadLockedFiles(currentChatId);
|
||||
logger.info(`Reapplied locked files for chat ID: ${currentChatId}`);
|
||||
}, 2000);
|
||||
|
||||
// Set up a periodic check to ensure locks remain applied
|
||||
setInterval(() => {
|
||||
this.#loadLockedFiles();
|
||||
const latestChatId = getCurrentChatId();
|
||||
this.#loadLockedFiles(latestChatId);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
|
@ -3,19 +3,54 @@ import { createScopedLogger } from './logger';
|
||||
|
||||
const logger = createScopedLogger('FileLocks');
|
||||
|
||||
/**
|
||||
* Get the current chat ID from the URL
|
||||
* @returns The current chat ID or a default value if not found
|
||||
*/
|
||||
export function getCurrentChatId(): string {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Extract chat ID from URL (format: /chat/123)
|
||||
const match = window.location.pathname.match(/\/chat\/([^/]+)/);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Return a default chat ID if none is found
|
||||
return 'default';
|
||||
} catch (error) {
|
||||
logger.error('Failed to get current chat ID', error);
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is locked directly from localStorage
|
||||
* This avoids circular dependencies between components and stores
|
||||
* @param filePath The path of the file to check
|
||||
* @param chatId Optional chat ID (will be extracted from URL if not provided)
|
||||
*/
|
||||
export function isFileLocked(filePath: string): { locked: boolean; lockMode?: LockMode } {
|
||||
export function isFileLocked(filePath: string, chatId?: string): { locked: boolean; lockMode?: LockMode } {
|
||||
try {
|
||||
const currentChatId = chatId || getCurrentChatId();
|
||||
const lockedFiles = getLockedFiles();
|
||||
const lockedFile = lockedFiles.find((file) => file.path === filePath);
|
||||
|
||||
// First check for a chat-specific lock
|
||||
const lockedFile = lockedFiles.find((file) => file.chatId === currentChatId && file.path === filePath);
|
||||
|
||||
if (lockedFile) {
|
||||
return { locked: true, lockMode: lockedFile.lockMode };
|
||||
}
|
||||
|
||||
// For backward compatibility, also check for legacy locks without chatId
|
||||
const legacyLock = lockedFiles.find((file) => !file.chatId && file.path === filePath);
|
||||
|
||||
if (legacyLock) {
|
||||
return { locked: true, lockMode: legacyLock.lockMode };
|
||||
}
|
||||
|
||||
return { locked: false };
|
||||
} catch (error) {
|
||||
logger.error('Failed to check if file is locked', error);
|
||||
|
Loading…
Reference in New Issue
Block a user