feat: submit file changes to the llm (#11)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user