feat: submit file changes to the llm (#11)

This commit is contained in:
Dominic Elm
2024-07-25 17:28:23 +02:00
committed by GitHub
parent a5ed695cb3
commit 2cb3f09947
18 changed files with 415 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
import { WORK_DIR } from '~/utils/constants';
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';
import { stripIndents } from '~/utils/stripIndent';
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
@@ -20,6 +20,50 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
Use 2 spaces for code indentation
</code_formatting_info>
<diff_spec>
For user-made file modifications, a \`<${MODIFICATIONS_TAG_NAME}>\` section will appear at the start of the user message. It will contain either \`<diff>\` or \`<file>\` elements for each modified file:
- \`<diff path="/some/file/path.ext">\`: Contains GNU unified diff format changes
- \`<file path="/some/file/path.ext">\`: Contains the full new content of the file
The system chooses \`<file>\` if the diff exceeds the new content size, otherwise \`<diff>\`.
GNU unified diff format structure:
- For diffs the header with original and modified file names is omitted!
- Changed sections start with @@ -X,Y +A,B @@ where:
- X: Original file starting line
- Y: Original file line count
- A: Modified file starting line
- B: Modified file line count
- (-) lines: Removed from original
- (+) lines: Added in modified version
- Unmarked lines: Unchanged context
Example:
<${MODIFICATIONS_TAG_NAME}>
<diff path="/home/project/src/main.js">
@@ -2,7 +2,10 @@
return a + b;
}
-console.log('Hello, World!');
+console.log('Hello, Bolt!');
+
function greet() {
- return 'Greetings!';
+ return 'Greetings!!';
}
+
+console.log('The End');
</diff>
<file path="/home/project/package.json">
// full file content here
</file>
</${MODIFICATIONS_TAG_NAME}>
</diff_spec>
<artifact_info>
Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
@@ -28,19 +72,21 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
- Folders to create if necessary
<artifact_instructions>
1. Think BEFORE creating an artifact
1. Think BEFORE creating an artifact.
2. The current working directory is \`${cwd}\`.
2. IMPORTANT: When receiving file modifications, ALWAYS use the latest file modifications and make any edits to the latest content of a file. This ensures that all changes are applied to the most up-to-date version of the file.
3. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
3. The current working directory is \`${cwd}\`.
4. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
4. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
5. Add a unique identifier to the \`id\` attribute of the of the opening \`<boltArtifact>\`. For updates, reuse the prior identifier. The identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
5. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
6. Use \`<boltAction>\` tags to define specific actions to perform.
6. Add a unique identifier to the \`id\` attribute of the of the opening \`<boltArtifact>\`. For updates, reuse the prior identifier. The identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
7. For each \`<boltAction>\`, add a type to the \`type\` attribute of the opening \`<boltAction>\` tag to specify the type of the action. Assign one of the following values to the \`type\` attribute:
7. Use \`<boltAction>\` tags to define specific actions to perform.
8. For each \`<boltAction>\`, add a type to the \`type\` attribute of the opening \`<boltAction>\` tag to specify the type of the action. Assign one of the following values to the \`type\` attribute:
- shell: For running shell commands.
@@ -50,19 +96,19 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
- file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
8. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
9. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
IMPORTANT: Add all required dependencies to the \`package.json\` already and try to avoid \`npm i <pkg>\` if possible!
10. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
11. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
11. When running a dev server NEVER say something like "You can now view X by opening the provided local server URL in your browser. The preview will be opened automatically or by the user manually!
12. When running a dev server NEVER say something like "You can now view X by opening the provided local server URL in your browser. The preview will be opened automatically or by the user manually!
12. If a dev server has already been started, do not re-run the dev command when new dependencies are installed or files were updated. Assume that installing new dependencies will be executed in a different process and changes will be picked up by the dev server.
13. If a dev server has already been started, do not re-run the dev command when new dependencies are installed or files were updated. Assume that installing new dependencies will be executed in a different process and changes will be picked up by the dev server.
13. ULTRA IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
14. IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
- Ensure code is clean, readable, and maintainable.
- Adhere to proper naming conventions and consistent formatting.

View File

@@ -37,20 +37,17 @@ export class ActionRunner {
#webcontainer: Promise<WebContainer>;
#currentExecutionPromise: Promise<void> = Promise.resolve();
actions: ActionsMap = import.meta.hot?.data.actions ?? map({});
actions: ActionsMap = map({});
constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise;
if (import.meta.hot) {
import.meta.hot.data.actions = this.actions;
}
}
addAction(data: ActionCallbackData) {
const { actionId } = data;
const action = this.actions.get()[actionId];
const actions = this.actions.get();
const action = actions[actionId];
if (action) {
// action already added

View File

@@ -10,7 +10,7 @@ export class EditorStore {
#filesStore: FilesStore;
selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map<EditorDocuments>({});
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map({});
currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {
if (!selectedFile) {
@@ -74,7 +74,7 @@ export class EditorStore {
});
}
updateFile(filePath: string, newContent: string | Uint8Array) {
updateFile(filePath: string, newContent: string) {
const documents = this.documents.get();
const documentState = documents[filePath];

View File

@@ -1,17 +1,22 @@
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
import { getEncoding } from 'istextorbinary';
import { map, type MapStore } from 'nanostores';
import { Buffer } from 'node:buffer';
import * as nodePath from 'node:path';
import { bufferWatchEvents } from '~/utils/buffer';
import { WORK_DIR } from '~/utils/constants';
import { computeFileModifications } from '~/utils/diff';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
const logger = createScopedLogger('FilesStore');
const textDecoder = new TextDecoder('utf8', { fatal: true });
const utf8TextDecoder = new TextDecoder('utf8', { fatal: true });
export interface File {
type: 'file';
content: string | Uint8Array;
content: string;
isBinary: boolean;
}
export interface Folder {
@@ -30,6 +35,16 @@ export class FilesStore {
*/
#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<string, string> = import.meta.hot?.data.modifiedFiles ?? new Map();
/**
* Map of files that matches the state of WebContainer.
*/
files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});
get filesCount() {
@@ -41,6 +56,7 @@ export class FilesStore {
if (import.meta.hot) {
import.meta.hot.data.files = this.files;
import.meta.hot.data.modifiedFiles = this.#modifiedFiles;
}
this.#init();
@@ -56,7 +72,15 @@ export class FilesStore {
return dirent;
}
async saveFile(filePath: string, content: string | Uint8Array) {
getFileModifications() {
return computeFileModifications(this.files.get(), this.#modifiedFiles);
}
resetFileModifications() {
this.#modifiedFiles.clear();
}
async saveFile(filePath: string, content: string) {
const webcontainer = await this.#webcontainer;
try {
@@ -66,9 +90,20 @@ export class FilesStore {
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
}
const oldContent = this.getFile(filePath)?.content;
if (!oldContent) {
unreachable('Expected content to be defined');
}
await webcontainer.fs.writeFile(relativePath, content);
this.files.setKey(filePath, { type: 'file', content });
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, { type: 'file', content, isBinary: false });
logger.info('File updated');
} catch (error) {
@@ -117,7 +152,21 @@ export class FilesStore {
this.#size++;
}
this.files.setKey(sanitizedPath, { type: 'file', content: this.#decodeFileContent(buffer) });
let content = '';
/**
* @note This check is purely for the editor. The way we detect this is not
* bullet-proof and it's a best guess so there might be false-positives.
* The reason we do this is because we don't want to display binary files
* in the editor nor allow to edit them.
*/
const isBinary = isBinaryFile(buffer);
if (!isBinary) {
content = this.#decodeFileContent(buffer);
}
this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
break;
}
@@ -140,10 +189,32 @@ export class FilesStore {
}
try {
return textDecoder.decode(buffer);
return utf8TextDecoder.decode(buffer);
} catch (error) {
console.log(error);
return '';
}
}
}
function isBinaryFile(buffer: Uint8Array | undefined) {
if (buffer === undefined) {
return false;
}
return getEncoding(convertToBuffer(buffer), { chunkLength: 100 }) === 'binary';
}
/**
* Converts a `Uint8Array` into a Node.js `Buffer` by copying the prototype.
* The goal is to avoid expensive copies. It does create a new typed array
* but that's generally cheap as long as it uses the same underlying
* array buffer.
*/
function convertToBuffer(view: Uint8Array): Buffer {
const buffer = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
Object.setPrototypeOf(buffer, Buffer.prototype);
return buffer as Buffer;
}

View File

@@ -70,7 +70,7 @@ export class WorkbenchStore {
this.showWorkbench.set(show);
}
setCurrentDocumentContent(newContent: string | Uint8Array) {
setCurrentDocumentContent(newContent: string) {
const filePath = this.currentDocument.get()?.filePath;
if (!filePath) {
@@ -119,6 +119,22 @@ export class WorkbenchStore {
this.#editorStore.setSelectedFile(filePath);
}
async saveFile(filePath: string) {
const documents = this.#editorStore.documents.get();
const document = documents[filePath];
if (document === undefined) {
return;
}
await this.#filesStore.saveFile(filePath, document.value);
const newUnsavedFiles = new Set(this.unsavedFiles.get());
newUnsavedFiles.delete(filePath);
this.unsavedFiles.set(newUnsavedFiles);
}
async saveCurrentDocument() {
const currentDocument = this.currentDocument.get();
@@ -126,14 +142,7 @@ export class WorkbenchStore {
return;
}
const { filePath } = currentDocument;
await this.#filesStore.saveFile(filePath, currentDocument.value);
const newUnsavedFiles = new Set(this.unsavedFiles.get());
newUnsavedFiles.delete(filePath);
this.unsavedFiles.set(newUnsavedFiles);
await this.saveFile(currentDocument.filePath);
}
resetCurrentDocument() {
@@ -153,6 +162,20 @@ export class WorkbenchStore {
this.setCurrentDocumentContent(file.content);
}
async saveAllFiles() {
for (const filePath of this.unsavedFiles.get()) {
await this.saveFile(filePath);
}
}
getFileModifcations() {
return this.#filesStore.getFileModifications();
}
resetAllFileModifications() {
this.#filesStore.resetFileModifications();
}
abortAllActions() {
// TODO: what do we wanna do and how do we wanna recover from this?
}