import { map, type MapStore } from 'nanostores'; import { computeFileModifications } from '~/utils/diff'; import { createScopedLogger } from '~/utils/logger'; import { unreachable } from '~/utils/unreachable'; import type { ProtocolFile } from '../replay/SimulationPrompt'; const logger = createScopedLogger('FilesStore'); export type FileMap = Record; export class FilesStore { /** * Tracks the number of files without folders. */ #size = 0; /** * @note Keeps track all modified files with their original content since the last user message. * Needs to be reset when the user sends another message and all changes have to be submitted * for the model to be aware of the changes. */ #modifiedFiles: Map = import.meta.hot?.data.modifiedFiles ?? new Map(); /** * Map of files that matches the state of WebContainer. */ files: MapStore = import.meta.hot?.data.files ?? map({}); get filesCount() { return this.#size; } constructor() { if (import.meta.hot) { import.meta.hot.data.files = this.files; import.meta.hot.data.modifiedFiles = this.#modifiedFiles; } } getFile(filePath: string) { const dirent = this.files.get()[filePath]; return dirent; } getFileModifications() { return computeFileModifications(this.files.get(), this.#modifiedFiles); } resetFileModifications() { this.#modifiedFiles.clear(); } async saveFile(filePath: string, content: string) { try { const oldContent = this.getFile(filePath)?.content; if (!oldContent) { console.log('CurrentFiles', JSON.stringify(Object.keys(this.files.get()))); unreachable(`Cannot save unknown file ${filePath}`); } if (!this.#modifiedFiles.has(filePath)) { this.#modifiedFiles.set(filePath, oldContent); } // we immediately update the file and don't rely on the `change` event coming from the watcher this.files.setKey(filePath, { path: filePath, content }); logger.info('File updated'); } catch (error) { logger.error('Failed to update file content\n\n', error); throw error; } } }