mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-03 11:51:36 +00:00
feat: submit file changes to the llm (#11)
This commit is contained in:
parent
a5ed695cb3
commit
2cb3f09947
@ -18,7 +18,7 @@ interface BaseChatProps {
|
|||||||
promptEnhanced?: boolean;
|
promptEnhanced?: boolean;
|
||||||
input?: string;
|
input?: string;
|
||||||
handleStop?: () => void;
|
handleStop?: () => void;
|
||||||
sendMessage?: () => void;
|
sendMessage?: (event: React.UIEvent) => void;
|
||||||
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
enhancePrompt?: () => void;
|
enhancePrompt?: () => void;
|
||||||
}
|
}
|
||||||
@ -103,7 +103,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
sendMessage?.();
|
sendMessage?.(event);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={input}
|
value={input}
|
||||||
@ -122,13 +122,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
<SendButton
|
<SendButton
|
||||||
show={input.length > 0 || isStreaming}
|
show={input.length > 0 || isStreaming}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
handleStop?.();
|
handleStop?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage?.();
|
sendMessage?.(event);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -2,10 +2,11 @@ import type { Message } from 'ai';
|
|||||||
import { useChat } from 'ai/react';
|
import { useChat } from 'ai/react';
|
||||||
import { useAnimate } from 'framer-motion';
|
import { useAnimate } from 'framer-motion';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { toast, ToastContainer, cssTransition } from 'react-toastify';
|
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||||
import { useMessageParser, usePromptEnhancer, useSnapScroll } from '~/lib/hooks';
|
import { useMessageParser, usePromptEnhancer, useSnapScroll } from '~/lib/hooks';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
|
import { fileModificationsToHTML } from '~/utils/diff';
|
||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
import { BaseChat } from './BaseChat';
|
import { BaseChat } from './BaseChat';
|
||||||
@ -41,7 +42,7 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
|
|||||||
|
|
||||||
const [animationScope, animate] = useAnimate();
|
const [animationScope, animate] = useAnimate();
|
||||||
|
|
||||||
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop } = useChat({
|
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop, append } = useChat({
|
||||||
api: '/api/chat',
|
api: '/api/chat',
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
@ -100,15 +101,49 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
|
|||||||
setChatStarted(true);
|
setChatStarted(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = async (event: React.UIEvent) => {
|
||||||
if (input.length === 0) {
|
if (input.length === 0 || isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note (delm) Usually saving files shouldn't take long but it may take longer if there
|
||||||
|
* many unsaved files. In that case we need to block user input and show an indicator
|
||||||
|
* of some kind so the user is aware that something is happening. But I consider the
|
||||||
|
* happy case to be no unsaved files and I would expect users to save their changes
|
||||||
|
* before they send another message.
|
||||||
|
*/
|
||||||
|
await workbenchStore.saveAllFiles();
|
||||||
|
|
||||||
|
const fileModifications = workbenchStore.getFileModifcations();
|
||||||
|
|
||||||
chatStore.setKey('aborted', false);
|
chatStore.setKey('aborted', false);
|
||||||
|
|
||||||
runAnimation();
|
runAnimation();
|
||||||
handleSubmit();
|
|
||||||
|
if (fileModifications !== undefined) {
|
||||||
|
const diff = fileModificationsToHTML(fileModifications);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have file modifications we append a new user message manually since we have to prefix
|
||||||
|
* the user input with the file modifications and we don't want the new user input to appear
|
||||||
|
* in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
|
||||||
|
* manually reset the input and we'd have to manually pass in file attachments. However, those
|
||||||
|
* aren't relevant here.
|
||||||
|
*/
|
||||||
|
append({ role: 'user', content: `${diff}\n\n${input}` });
|
||||||
|
|
||||||
|
setInput('');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After sending a new message we reset all modifications since the model
|
||||||
|
* should now be aware of all the changes.
|
||||||
|
*/
|
||||||
|
workbenchStore.resetAllFileModifications();
|
||||||
|
} else {
|
||||||
|
handleSubmit(event);
|
||||||
|
}
|
||||||
|
|
||||||
resetEnhancer();
|
resetEnhancer();
|
||||||
|
|
||||||
textareaRef.current?.blur();
|
textareaRef.current?.blur();
|
||||||
|
@ -3,7 +3,7 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
|
|||||||
interface SendButtonProps {
|
interface SendButtonProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
onClick?: VoidFunction;
|
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
||||||
@ -20,7 +20,7 @@ export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
|
|||||||
exit={{ opacity: 0, y: 10 }}
|
exit={{ opacity: 0, y: 10 }}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onClick?.();
|
onClick?.(event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { modificationsRegex } from '~/utils/diff';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
@ -7,7 +8,11 @@ interface UserMessageProps {
|
|||||||
export function UserMessage({ content }: UserMessageProps) {
|
export function UserMessage({ content }: UserMessageProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<Markdown>{content}</Markdown>
|
<Markdown>{sanitizeUserMessage(content)}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeUserMessage(content: string) {
|
||||||
|
return content.replace(modificationsRegex, '').trim();
|
||||||
|
}
|
||||||
|
@ -26,7 +26,8 @@ import { getLanguage } from './languages';
|
|||||||
const logger = createScopedLogger('CodeMirrorEditor');
|
const logger = createScopedLogger('CodeMirrorEditor');
|
||||||
|
|
||||||
export interface EditorDocument {
|
export interface EditorDocument {
|
||||||
value: string | Uint8Array;
|
value: string;
|
||||||
|
isBinary: boolean;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
scroll?: ScrollPosition;
|
scroll?: ScrollPosition;
|
||||||
}
|
}
|
||||||
@ -116,8 +117,6 @@ export const CodeMirrorEditor = memo(
|
|||||||
const onChangeRef = useRef(onChange);
|
const onChangeRef = useRef(onChange);
|
||||||
const onSaveRef = useRef(onSave);
|
const onSaveRef = useRef(onSave);
|
||||||
|
|
||||||
const isBinaryFile = doc?.value instanceof Uint8Array;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This effect is used to avoid side effects directly in the render function
|
* This effect is used to avoid side effects directly in the render function
|
||||||
* and instead the refs are updated after each render.
|
* and instead the refs are updated after each render.
|
||||||
@ -198,7 +197,7 @@ export const CodeMirrorEditor = memo(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.value instanceof Uint8Array) {
|
if (doc.isBinary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +229,7 @@ export const CodeMirrorEditor = memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('relative h-full', className)}>
|
<div className={classNames('relative h-full', className)}>
|
||||||
{isBinaryFile && <BinaryContent />}
|
{doc?.isBinary && <BinaryContent />}
|
||||||
<div className="h-full overflow-hidden" ref={containerRef} />
|
<div className="h-full overflow-hidden" ref={containerRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -343,7 +342,7 @@ function setEditorDocument(
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: [editableStateEffect.of(editable)],
|
effects: [editableStateEffect.of(editable && !doc.isBinary)],
|
||||||
});
|
});
|
||||||
|
|
||||||
getLanguage(doc.filePath).then((languageSupport) => {
|
getLanguage(doc.filePath).then((languageSupport) => {
|
||||||
|
@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center gap-1.5 px-1.5 rounded-lg py-0.5 bg-transparent hover:bg-white disabled:cursor-not-allowed',
|
'flex items-center gap-1.5 px-1.5 rounded-md py-0.5 bg-transparent hover:bg-white disabled:cursor-not-allowed',
|
||||||
{
|
{
|
||||||
[classNames('opacity-30', disabledClassName)]: disabled,
|
[classNames('opacity-30', disabledClassName)]: disabled,
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { WORK_DIR } from '~/utils/constants';
|
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';
|
||||||
import { stripIndents } from '~/utils/stripIndent';
|
import { stripIndents } from '~/utils/stripIndent';
|
||||||
|
|
||||||
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
|
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
|
Use 2 spaces for code indentation
|
||||||
</code_formatting_info>
|
</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>
|
<artifact_info>
|
||||||
Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
|
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
|
- Folders to create if necessary
|
||||||
|
|
||||||
<artifact_instructions>
|
<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.
|
- 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.
|
- 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!
|
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.
|
- Ensure code is clean, readable, and maintainable.
|
||||||
- Adhere to proper naming conventions and consistent formatting.
|
- Adhere to proper naming conventions and consistent formatting.
|
||||||
|
@ -37,20 +37,17 @@ export class ActionRunner {
|
|||||||
#webcontainer: Promise<WebContainer>;
|
#webcontainer: Promise<WebContainer>;
|
||||||
#currentExecutionPromise: Promise<void> = Promise.resolve();
|
#currentExecutionPromise: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
actions: ActionsMap = import.meta.hot?.data.actions ?? map({});
|
actions: ActionsMap = map({});
|
||||||
|
|
||||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||||
this.#webcontainer = webcontainerPromise;
|
this.#webcontainer = webcontainerPromise;
|
||||||
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.data.actions = this.actions;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addAction(data: ActionCallbackData) {
|
addAction(data: ActionCallbackData) {
|
||||||
const { actionId } = data;
|
const { actionId } = data;
|
||||||
|
|
||||||
const action = this.actions.get()[actionId];
|
const actions = this.actions.get();
|
||||||
|
const action = actions[actionId];
|
||||||
|
|
||||||
if (action) {
|
if (action) {
|
||||||
// action already added
|
// action already added
|
||||||
|
@ -10,7 +10,7 @@ export class EditorStore {
|
|||||||
#filesStore: FilesStore;
|
#filesStore: FilesStore;
|
||||||
|
|
||||||
selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();
|
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) => {
|
currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {
|
||||||
if (!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 documents = this.documents.get();
|
||||||
const documentState = documents[filePath];
|
const documentState = documents[filePath];
|
||||||
|
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
||||||
|
import { getEncoding } from 'istextorbinary';
|
||||||
import { map, type MapStore } from 'nanostores';
|
import { map, type MapStore } from 'nanostores';
|
||||||
|
import { Buffer } from 'node:buffer';
|
||||||
import * as nodePath from 'node:path';
|
import * as nodePath from 'node:path';
|
||||||
import { bufferWatchEvents } from '~/utils/buffer';
|
import { bufferWatchEvents } from '~/utils/buffer';
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
|
import { computeFileModifications } from '~/utils/diff';
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
|
import { unreachable } from '~/utils/unreachable';
|
||||||
|
|
||||||
const logger = createScopedLogger('FilesStore');
|
const logger = createScopedLogger('FilesStore');
|
||||||
|
|
||||||
const textDecoder = new TextDecoder('utf8', { fatal: true });
|
const utf8TextDecoder = new TextDecoder('utf8', { fatal: true });
|
||||||
|
|
||||||
export interface File {
|
export interface File {
|
||||||
type: 'file';
|
type: 'file';
|
||||||
content: string | Uint8Array;
|
content: string;
|
||||||
|
isBinary: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Folder {
|
export interface Folder {
|
||||||
@ -30,6 +35,16 @@ export class FilesStore {
|
|||||||
*/
|
*/
|
||||||
#size = 0;
|
#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({});
|
files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});
|
||||||
|
|
||||||
get filesCount() {
|
get filesCount() {
|
||||||
@ -41,6 +56,7 @@ export class FilesStore {
|
|||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
import.meta.hot.data.files = this.files;
|
import.meta.hot.data.files = this.files;
|
||||||
|
import.meta.hot.data.modifiedFiles = this.#modifiedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#init();
|
this.#init();
|
||||||
@ -56,7 +72,15 @@ export class FilesStore {
|
|||||||
return dirent;
|
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;
|
const webcontainer = await this.#webcontainer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -66,9 +90,20 @@ export class FilesStore {
|
|||||||
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
|
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);
|
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');
|
logger.info('File updated');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,7 +152,21 @@ export class FilesStore {
|
|||||||
this.#size++;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@ -140,10 +189,32 @@ export class FilesStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return textDecoder.decode(buffer);
|
return utf8TextDecoder.decode(buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return '';
|
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);
|
this.showWorkbench.set(show);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentDocumentContent(newContent: string | Uint8Array) {
|
setCurrentDocumentContent(newContent: string) {
|
||||||
const filePath = this.currentDocument.get()?.filePath;
|
const filePath = this.currentDocument.get()?.filePath;
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
@ -119,6 +119,22 @@ export class WorkbenchStore {
|
|||||||
this.#editorStore.setSelectedFile(filePath);
|
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() {
|
async saveCurrentDocument() {
|
||||||
const currentDocument = this.currentDocument.get();
|
const currentDocument = this.currentDocument.get();
|
||||||
|
|
||||||
@ -126,14 +142,7 @@ export class WorkbenchStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filePath } = currentDocument;
|
await this.saveFile(currentDocument.filePath);
|
||||||
|
|
||||||
await this.#filesStore.saveFile(filePath, currentDocument.value);
|
|
||||||
|
|
||||||
const newUnsavedFiles = new Set(this.unsavedFiles.get());
|
|
||||||
newUnsavedFiles.delete(filePath);
|
|
||||||
|
|
||||||
this.unsavedFiles.set(newUnsavedFiles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCurrentDocument() {
|
resetCurrentDocument() {
|
||||||
@ -153,6 +162,20 @@ export class WorkbenchStore {
|
|||||||
this.setCurrentDocumentContent(file.content);
|
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() {
|
abortAllActions() {
|
||||||
// TODO: what do we wanna do and how do we wanna recover from this?
|
// TODO: what do we wanna do and how do we wanna recover from this?
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
|||||||
|
|
||||||
export async function action({ context, request }: ActionFunctionArgs) {
|
export async function action({ context, request }: ActionFunctionArgs) {
|
||||||
const { messages } = await request.json<{ messages: Messages }>();
|
const { messages } = await request.json<{ messages: Messages }>();
|
||||||
|
|
||||||
const stream = new SwitchableStream();
|
const stream = new SwitchableStream();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export const WORK_DIR_NAME = 'project';
|
export const WORK_DIR_NAME = 'project';
|
||||||
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
||||||
|
export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
|
||||||
|
108
packages/bolt/app/utils/diff.ts
Normal file
108
packages/bolt/app/utils/diff.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { createTwoFilesPatch } from 'diff';
|
||||||
|
import type { FileMap } from '~/lib/stores/files';
|
||||||
|
import { MODIFICATIONS_TAG_NAME } from './constants';
|
||||||
|
|
||||||
|
export const modificationsRegex = new RegExp(
|
||||||
|
`^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
|
||||||
|
'g',
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModifiedFile {
|
||||||
|
type: 'diff' | 'file';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileModifications = Record<string, ModifiedFile>;
|
||||||
|
|
||||||
|
export function computeFileModifications(files: FileMap, modifiedFiles: Map<string, string>) {
|
||||||
|
const modifications: FileModifications = {};
|
||||||
|
|
||||||
|
let hasModifiedFiles = false;
|
||||||
|
|
||||||
|
for (const [filePath, originalContent] of modifiedFiles) {
|
||||||
|
const file = files[filePath];
|
||||||
|
|
||||||
|
if (file?.type !== 'file') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unifiedDiff = diffFiles(filePath, originalContent, file.content);
|
||||||
|
|
||||||
|
if (!unifiedDiff) {
|
||||||
|
// files are identical
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasModifiedFiles = true;
|
||||||
|
|
||||||
|
if (unifiedDiff.length > file.content.length) {
|
||||||
|
// if there are lots of changes we simply grab the current file content since it's smaller than the diff
|
||||||
|
modifications[filePath] = { type: 'file', content: file.content };
|
||||||
|
} else {
|
||||||
|
// otherwise we use the diff since it's smaller
|
||||||
|
modifications[filePath] = { type: 'diff', content: unifiedDiff };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasModifiedFiles) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a diff in the unified format. The only difference is that the header is omitted
|
||||||
|
* because it will always assume that you're comparing two versions of the same file and
|
||||||
|
* it allows us to avoid the extra characters we send back to the llm.
|
||||||
|
*
|
||||||
|
* @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html
|
||||||
|
*/
|
||||||
|
export function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) {
|
||||||
|
let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent);
|
||||||
|
|
||||||
|
const patchHeaderEnd = `--- ${fileName}\n+++ ${fileName}\n`;
|
||||||
|
const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd);
|
||||||
|
|
||||||
|
if (headerEndIndex >= 0) {
|
||||||
|
unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unifiedDiff === '') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unifiedDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the unified diff to HTML.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <bolt_file_modifications>
|
||||||
|
* <diff path="/home/project/index.js">
|
||||||
|
* - console.log('Hello, World!');
|
||||||
|
* + console.log('Hello, Bolt!');
|
||||||
|
* </diff>
|
||||||
|
* </bolt_file_modifications>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function fileModificationsToHTML(modifications: FileModifications) {
|
||||||
|
const entries = Object.entries(modifications);
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`];
|
||||||
|
|
||||||
|
for (const [filePath, { type, content }] of entries) {
|
||||||
|
result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `</${type}>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(`</${MODIFICATIONS_TAG_NAME}>`);
|
||||||
|
|
||||||
|
return result.join('\n');
|
||||||
|
}
|
@ -43,8 +43,10 @@
|
|||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"ai": "^3.2.27",
|
"ai": "^3.2.27",
|
||||||
|
"diff": "^5.2.0",
|
||||||
"framer-motion": "^11.2.12",
|
"framer-motion": "^11.2.12",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"istextorbinary": "^9.5.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.10.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -59,6 +61,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"@remix-run/dev": "^2.10.0",
|
"@remix-run/dev": "^2.10.0",
|
||||||
|
"@types/diff": "^5.2.1",
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
|
15
packages/bolt/types/istextorbinary.d.ts
vendored
Normal file
15
packages/bolt/types/istextorbinary.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @note For some reason the types aren't picked up from node_modules so I declared the module here
|
||||||
|
* with only the function that we use.
|
||||||
|
*/
|
||||||
|
declare module 'istextorbinary' {
|
||||||
|
export interface EncodingOpts {
|
||||||
|
/** Defaults to 24 */
|
||||||
|
chunkLength?: number;
|
||||||
|
|
||||||
|
/** If not provided, will check the start, beginning, and end */
|
||||||
|
chunkBegin?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEncoding(buffer: Buffer | null, opts?: EncodingOpts): 'utf8' | 'binary' | null;
|
||||||
|
}
|
@ -12,7 +12,7 @@ export default defineConfig((config) => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
nodePolyfills({
|
nodePolyfills({
|
||||||
include: ['path'],
|
include: ['path', 'buffer'],
|
||||||
}),
|
}),
|
||||||
config.mode !== 'test' && remixCloudflareDevProxy(),
|
config.mode !== 'test' && remixCloudflareDevProxy(),
|
||||||
remixVitePlugin({
|
remixVitePlugin({
|
||||||
|
@ -116,12 +116,18 @@ importers:
|
|||||||
ai:
|
ai:
|
||||||
specifier: ^3.2.27
|
specifier: ^3.2.27
|
||||||
version: 3.2.27(react@18.3.1)(svelte@4.2.18)(vue@3.4.30(typescript@5.5.2))(zod@3.23.8)
|
version: 3.2.27(react@18.3.1)(svelte@4.2.18)(vue@3.4.30(typescript@5.5.2))(zod@3.23.8)
|
||||||
|
diff:
|
||||||
|
specifier: ^5.2.0
|
||||||
|
version: 5.2.0
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^11.2.12
|
specifier: ^11.2.12
|
||||||
version: 11.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 11.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
isbot:
|
isbot:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
|
istextorbinary:
|
||||||
|
specifier: ^9.5.0
|
||||||
|
version: 9.5.0
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.10.3
|
specifier: ^0.10.3
|
||||||
version: 0.10.3
|
version: 0.10.3
|
||||||
@ -159,6 +165,9 @@ importers:
|
|||||||
'@remix-run/dev':
|
'@remix-run/dev':
|
||||||
specifier: ^2.10.0
|
specifier: ^2.10.0
|
||||||
version: 2.10.0(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/serve@2.10.0(typescript@5.5.2))(@types/node@20.14.9)(sass@1.77.6)(typescript@5.5.2)(vite@5.3.1(@types/node@20.14.9)(sass@1.77.6))(wrangler@3.63.2(@cloudflare/workers-types@4.20240620.0))
|
version: 2.10.0(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/serve@2.10.0(typescript@5.5.2))(@types/node@20.14.9)(sass@1.77.6)(typescript@5.5.2)(vite@5.3.1(@types/node@20.14.9)(sass@1.77.6))(wrangler@3.63.2(@cloudflare/workers-types@4.20240620.0))
|
||||||
|
'@types/diff':
|
||||||
|
specifier: ^5.2.1
|
||||||
|
version: 5.2.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.20
|
specifier: ^18.2.20
|
||||||
version: 18.3.3
|
version: 18.3.3
|
||||||
@ -1445,6 +1454,9 @@ packages:
|
|||||||
'@types/diff-match-patch@1.0.36':
|
'@types/diff-match-patch@1.0.36':
|
||||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||||
|
|
||||||
|
'@types/diff@5.2.1':
|
||||||
|
resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==}
|
||||||
|
|
||||||
'@types/eslint@8.56.10':
|
'@types/eslint@8.56.10':
|
||||||
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
||||||
|
|
||||||
@ -1865,6 +1877,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
binaryextensions@6.11.0:
|
||||||
|
resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
@ -2328,6 +2344,10 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
editions@6.21.0:
|
||||||
|
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@ -3040,6 +3060,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
|
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
istextorbinary@9.5.0:
|
||||||
|
resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
jackspeak@3.4.0:
|
jackspeak@3.4.0:
|
||||||
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
|
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@ -4522,6 +4546,10 @@ packages:
|
|||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
|
textextensions@6.11.0:
|
||||||
|
resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
through2@2.0.5:
|
through2@2.0.5:
|
||||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
||||||
|
|
||||||
@ -4766,6 +4794,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
version-range@4.14.0:
|
||||||
|
resolution: {integrity: sha512-gjb0ARm9qlcBAonU4zPwkl9ecKkas+tC2CGwFfptTCWWIVTWY1YUbT2zZKsOAF1jR/tNxxyLwwG0cb42XlYcTg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
vfile-location@5.0.2:
|
vfile-location@5.0.2:
|
||||||
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
|
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
|
||||||
|
|
||||||
@ -6344,6 +6376,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/diff-match-patch@1.0.36': {}
|
'@types/diff-match-patch@1.0.36': {}
|
||||||
|
|
||||||
|
'@types/diff@5.2.1': {}
|
||||||
|
|
||||||
'@types/eslint@8.56.10':
|
'@types/eslint@8.56.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.5
|
||||||
@ -6933,6 +6967,10 @@ snapshots:
|
|||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
binaryextensions@6.11.0:
|
||||||
|
dependencies:
|
||||||
|
editions: 6.21.0
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
@ -7435,6 +7473,10 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
editions@6.21.0:
|
||||||
|
dependencies:
|
||||||
|
version-range: 4.14.0
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.4.812: {}
|
electron-to-chromium@1.4.812: {}
|
||||||
@ -8284,6 +8326,12 @@ snapshots:
|
|||||||
|
|
||||||
isomorphic-timers-promises@1.0.1: {}
|
isomorphic-timers-promises@1.0.1: {}
|
||||||
|
|
||||||
|
istextorbinary@9.5.0:
|
||||||
|
dependencies:
|
||||||
|
binaryextensions: 6.11.0
|
||||||
|
editions: 6.21.0
|
||||||
|
textextensions: 6.11.0
|
||||||
|
|
||||||
jackspeak@3.4.0:
|
jackspeak@3.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
@ -10228,6 +10276,10 @@ snapshots:
|
|||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
|
textextensions@6.11.0:
|
||||||
|
dependencies:
|
||||||
|
editions: 6.21.0
|
||||||
|
|
||||||
through2@2.0.5:
|
through2@2.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
@ -10502,6 +10554,8 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
version-range@4.14.0: {}
|
||||||
|
|
||||||
vfile-location@5.0.2:
|
vfile-location@5.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.2
|
'@types/unist': 3.0.2
|
||||||
|
Loading…
Reference in New Issue
Block a user