Dominic Elm
commited on
feat: submit file changes to the llm (#11)
Browse files- packages/bolt/app/components/chat/BaseChat.tsx +4 -4
- packages/bolt/app/components/chat/Chat.client.tsx +40 -5
- packages/bolt/app/components/chat/SendButton.client.tsx +2 -2
- packages/bolt/app/components/chat/UserMessage.tsx +6 -1
- packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx +5 -6
- packages/bolt/app/components/ui/PanelHeaderButton.tsx +1 -1
- packages/bolt/app/lib/.server/llm/prompts.ts +60 -14
- packages/bolt/app/lib/runtime/action-runner.ts +3 -6
- packages/bolt/app/lib/stores/editor.ts +2 -2
- packages/bolt/app/lib/stores/files.ts +77 -6
- packages/bolt/app/lib/stores/workbench.ts +30 -7
- packages/bolt/app/routes/api.chat.ts +1 -0
- packages/bolt/app/utils/constants.ts +1 -0
- packages/bolt/app/utils/diff.ts +108 -0
- packages/bolt/package.json +3 -0
- packages/bolt/types/istextorbinary.d.ts +15 -0
- packages/bolt/vite.config.ts +1 -1
- pnpm-lock.yaml +54 -0
packages/bolt/app/components/chat/BaseChat.tsx
CHANGED
@@ -18,7 +18,7 @@ interface BaseChatProps {
|
|
18 |
promptEnhanced?: boolean;
|
19 |
input?: string;
|
20 |
handleStop?: () => void;
|
21 |
-
sendMessage?: () => void;
|
22 |
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
23 |
enhancePrompt?: () => void;
|
24 |
}
|
@@ -103,7 +103,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
103 |
|
104 |
event.preventDefault();
|
105 |
|
106 |
-
sendMessage?.();
|
107 |
}
|
108 |
}}
|
109 |
value={input}
|
@@ -122,13 +122,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
122 |
<SendButton
|
123 |
show={input.length > 0 || isStreaming}
|
124 |
isStreaming={isStreaming}
|
125 |
-
onClick={() => {
|
126 |
if (isStreaming) {
|
127 |
handleStop?.();
|
128 |
return;
|
129 |
}
|
130 |
|
131 |
-
sendMessage?.();
|
132 |
}}
|
133 |
/>
|
134 |
)}
|
|
|
18 |
promptEnhanced?: boolean;
|
19 |
input?: string;
|
20 |
handleStop?: () => void;
|
21 |
+
sendMessage?: (event: React.UIEvent) => void;
|
22 |
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
23 |
enhancePrompt?: () => void;
|
24 |
}
|
|
|
103 |
|
104 |
event.preventDefault();
|
105 |
|
106 |
+
sendMessage?.(event);
|
107 |
}
|
108 |
}}
|
109 |
value={input}
|
|
|
122 |
<SendButton
|
123 |
show={input.length > 0 || isStreaming}
|
124 |
isStreaming={isStreaming}
|
125 |
+
onClick={(event) => {
|
126 |
if (isStreaming) {
|
127 |
handleStop?.();
|
128 |
return;
|
129 |
}
|
130 |
|
131 |
+
sendMessage?.(event);
|
132 |
}}
|
133 |
/>
|
134 |
)}
|
packages/bolt/app/components/chat/Chat.client.tsx
CHANGED
@@ -2,10 +2,11 @@ import type { Message } from 'ai';
|
|
2 |
import { useChat } from 'ai/react';
|
3 |
import { useAnimate } from 'framer-motion';
|
4 |
import { useEffect, useRef, useState } from 'react';
|
5 |
-
import { toast, ToastContainer
|
6 |
import { useMessageParser, usePromptEnhancer, useSnapScroll } from '~/lib/hooks';
|
7 |
import { chatStore } from '~/lib/stores/chat';
|
8 |
import { workbenchStore } from '~/lib/stores/workbench';
|
|
|
9 |
import { cubicEasingFn } from '~/utils/easings';
|
10 |
import { createScopedLogger } from '~/utils/logger';
|
11 |
import { BaseChat } from './BaseChat';
|
@@ -41,7 +42,7 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
|
|
41 |
|
42 |
const [animationScope, animate] = useAnimate();
|
43 |
|
44 |
-
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop } = useChat({
|
45 |
api: '/api/chat',
|
46 |
onError: (error) => {
|
47 |
logger.error(error);
|
@@ -100,15 +101,49 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
|
|
100 |
setChatStarted(true);
|
101 |
};
|
102 |
|
103 |
-
const sendMessage = () => {
|
104 |
-
if (input.length === 0) {
|
105 |
return;
|
106 |
}
|
107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
chatStore.setKey('aborted', false);
|
109 |
|
110 |
runAnimation();
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
resetEnhancer();
|
113 |
|
114 |
textareaRef.current?.blur();
|
|
|
2 |
import { useChat } from 'ai/react';
|
3 |
import { useAnimate } from 'framer-motion';
|
4 |
import { useEffect, useRef, useState } from 'react';
|
5 |
+
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
6 |
import { useMessageParser, usePromptEnhancer, useSnapScroll } from '~/lib/hooks';
|
7 |
import { chatStore } from '~/lib/stores/chat';
|
8 |
import { workbenchStore } from '~/lib/stores/workbench';
|
9 |
+
import { fileModificationsToHTML } from '~/utils/diff';
|
10 |
import { cubicEasingFn } from '~/utils/easings';
|
11 |
import { createScopedLogger } from '~/utils/logger';
|
12 |
import { BaseChat } from './BaseChat';
|
|
|
42 |
|
43 |
const [animationScope, animate] = useAnimate();
|
44 |
|
45 |
+
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop, append } = useChat({
|
46 |
api: '/api/chat',
|
47 |
onError: (error) => {
|
48 |
logger.error(error);
|
|
|
101 |
setChatStarted(true);
|
102 |
};
|
103 |
|
104 |
+
const sendMessage = async (event: React.UIEvent) => {
|
105 |
+
if (input.length === 0 || isLoading) {
|
106 |
return;
|
107 |
}
|
108 |
|
109 |
+
/**
|
110 |
+
* @note (delm) Usually saving files shouldn't take long but it may take longer if there
|
111 |
+
* many unsaved files. In that case we need to block user input and show an indicator
|
112 |
+
* of some kind so the user is aware that something is happening. But I consider the
|
113 |
+
* happy case to be no unsaved files and I would expect users to save their changes
|
114 |
+
* before they send another message.
|
115 |
+
*/
|
116 |
+
await workbenchStore.saveAllFiles();
|
117 |
+
|
118 |
+
const fileModifications = workbenchStore.getFileModifcations();
|
119 |
+
|
120 |
chatStore.setKey('aborted', false);
|
121 |
|
122 |
runAnimation();
|
123 |
+
|
124 |
+
if (fileModifications !== undefined) {
|
125 |
+
const diff = fileModificationsToHTML(fileModifications);
|
126 |
+
|
127 |
+
/**
|
128 |
+
* If we have file modifications we append a new user message manually since we have to prefix
|
129 |
+
* the user input with the file modifications and we don't want the new user input to appear
|
130 |
+
* in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
|
131 |
+
* manually reset the input and we'd have to manually pass in file attachments. However, those
|
132 |
+
* aren't relevant here.
|
133 |
+
*/
|
134 |
+
append({ role: 'user', content: `${diff}\n\n${input}` });
|
135 |
+
|
136 |
+
setInput('');
|
137 |
+
|
138 |
+
/**
|
139 |
+
* After sending a new message we reset all modifications since the model
|
140 |
+
* should now be aware of all the changes.
|
141 |
+
*/
|
142 |
+
workbenchStore.resetAllFileModifications();
|
143 |
+
} else {
|
144 |
+
handleSubmit(event);
|
145 |
+
}
|
146 |
+
|
147 |
resetEnhancer();
|
148 |
|
149 |
textareaRef.current?.blur();
|
packages/bolt/app/components/chat/SendButton.client.tsx
CHANGED
@@ -3,7 +3,7 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
|
|
3 |
interface SendButtonProps {
|
4 |
show: boolean;
|
5 |
isStreaming?: boolean;
|
6 |
-
onClick?:
|
7 |
}
|
8 |
|
9 |
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
@@ -20,7 +20,7 @@ export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
|
|
20 |
exit={{ opacity: 0, y: 10 }}
|
21 |
onClick={(event) => {
|
22 |
event.preventDefault();
|
23 |
-
onClick?.();
|
24 |
}}
|
25 |
>
|
26 |
<div className="text-lg">
|
|
|
3 |
interface SendButtonProps {
|
4 |
show: boolean;
|
5 |
isStreaming?: boolean;
|
6 |
+
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
7 |
}
|
8 |
|
9 |
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
|
|
20 |
exit={{ opacity: 0, y: 10 }}
|
21 |
onClick={(event) => {
|
22 |
event.preventDefault();
|
23 |
+
onClick?.(event);
|
24 |
}}
|
25 |
>
|
26 |
<div className="text-lg">
|
packages/bolt/app/components/chat/UserMessage.tsx
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
import { Markdown } from './Markdown';
|
2 |
|
3 |
interface UserMessageProps {
|
@@ -7,7 +8,11 @@ interface UserMessageProps {
|
|
7 |
export function UserMessage({ content }: UserMessageProps) {
|
8 |
return (
|
9 |
<div className="overflow-hidden">
|
10 |
-
<Markdown>{content}</Markdown>
|
11 |
</div>
|
12 |
);
|
13 |
}
|
|
|
|
|
|
|
|
|
|
1 |
+
import { modificationsRegex } from '~/utils/diff';
|
2 |
import { Markdown } from './Markdown';
|
3 |
|
4 |
interface UserMessageProps {
|
|
|
8 |
export function UserMessage({ content }: UserMessageProps) {
|
9 |
return (
|
10 |
<div className="overflow-hidden">
|
11 |
+
<Markdown>{sanitizeUserMessage(content)}</Markdown>
|
12 |
</div>
|
13 |
);
|
14 |
}
|
15 |
+
|
16 |
+
function sanitizeUserMessage(content: string) {
|
17 |
+
return content.replace(modificationsRegex, '').trim();
|
18 |
+
}
|
packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx
CHANGED
@@ -26,7 +26,8 @@ import { getLanguage } from './languages';
|
|
26 |
const logger = createScopedLogger('CodeMirrorEditor');
|
27 |
|
28 |
export interface EditorDocument {
|
29 |
-
value: string
|
|
|
30 |
filePath: string;
|
31 |
scroll?: ScrollPosition;
|
32 |
}
|
@@ -116,8 +117,6 @@ export const CodeMirrorEditor = memo(
|
|
116 |
const onChangeRef = useRef(onChange);
|
117 |
const onSaveRef = useRef(onSave);
|
118 |
|
119 |
-
const isBinaryFile = doc?.value instanceof Uint8Array;
|
120 |
-
|
121 |
/**
|
122 |
* This effect is used to avoid side effects directly in the render function
|
123 |
* and instead the refs are updated after each render.
|
@@ -198,7 +197,7 @@ export const CodeMirrorEditor = memo(
|
|
198 |
return;
|
199 |
}
|
200 |
|
201 |
-
if (doc.
|
202 |
return;
|
203 |
}
|
204 |
|
@@ -230,7 +229,7 @@ export const CodeMirrorEditor = memo(
|
|
230 |
|
231 |
return (
|
232 |
<div className={classNames('relative h-full', className)}>
|
233 |
-
{
|
234 |
<div className="h-full overflow-hidden" ref={containerRef} />
|
235 |
</div>
|
236 |
);
|
@@ -343,7 +342,7 @@ function setEditorDocument(
|
|
343 |
}
|
344 |
|
345 |
view.dispatch({
|
346 |
-
effects: [editableStateEffect.of(editable)],
|
347 |
});
|
348 |
|
349 |
getLanguage(doc.filePath).then((languageSupport) => {
|
|
|
26 |
const logger = createScopedLogger('CodeMirrorEditor');
|
27 |
|
28 |
export interface EditorDocument {
|
29 |
+
value: string;
|
30 |
+
isBinary: boolean;
|
31 |
filePath: string;
|
32 |
scroll?: ScrollPosition;
|
33 |
}
|
|
|
117 |
const onChangeRef = useRef(onChange);
|
118 |
const onSaveRef = useRef(onSave);
|
119 |
|
|
|
|
|
120 |
/**
|
121 |
* This effect is used to avoid side effects directly in the render function
|
122 |
* and instead the refs are updated after each render.
|
|
|
197 |
return;
|
198 |
}
|
199 |
|
200 |
+
if (doc.isBinary) {
|
201 |
return;
|
202 |
}
|
203 |
|
|
|
229 |
|
230 |
return (
|
231 |
<div className={classNames('relative h-full', className)}>
|
232 |
+
{doc?.isBinary && <BinaryContent />}
|
233 |
<div className="h-full overflow-hidden" ref={containerRef} />
|
234 |
</div>
|
235 |
);
|
|
|
342 |
}
|
343 |
|
344 |
view.dispatch({
|
345 |
+
effects: [editableStateEffect.of(editable && !doc.isBinary)],
|
346 |
});
|
347 |
|
348 |
getLanguage(doc.filePath).then((languageSupport) => {
|
packages/bolt/app/components/ui/PanelHeaderButton.tsx
CHANGED
@@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
|
|
14 |
return (
|
15 |
<button
|
16 |
className={classNames(
|
17 |
-
'flex items-center gap-1.5 px-1.5 rounded-
|
18 |
{
|
19 |
[classNames('opacity-30', disabledClassName)]: disabled,
|
20 |
},
|
|
|
14 |
return (
|
15 |
<button
|
16 |
className={classNames(
|
17 |
+
'flex items-center gap-1.5 px-1.5 rounded-md py-0.5 bg-transparent hover:bg-white disabled:cursor-not-allowed',
|
18 |
{
|
19 |
[classNames('opacity-30', disabledClassName)]: disabled,
|
20 |
},
|
packages/bolt/app/lib/.server/llm/prompts.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { WORK_DIR } from '~/utils/constants';
|
2 |
import { stripIndents } from '~/utils/stripIndent';
|
3 |
|
4 |
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
|
@@ -20,6 +20,50 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
20 |
Use 2 spaces for code indentation
|
21 |
</code_formatting_info>
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
<artifact_info>
|
24 |
Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
|
25 |
|
@@ -28,19 +72,21 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
28 |
- Folders to create if necessary
|
29 |
|
30 |
<artifact_instructions>
|
31 |
-
1. Think BEFORE creating an artifact
|
|
|
|
|
32 |
|
33 |
-
|
34 |
|
35 |
-
|
36 |
|
37 |
-
|
38 |
|
39 |
-
|
40 |
|
41 |
-
|
42 |
|
43 |
-
|
44 |
|
45 |
- shell: For running shell commands.
|
46 |
|
@@ -50,19 +96,19 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
50 |
|
51 |
- 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.
|
52 |
|
53 |
-
|
54 |
|
55 |
-
|
56 |
|
57 |
IMPORTANT: Add all required dependencies to the \`package.json\` already and try to avoid \`npm i <pkg>\` if possible!
|
58 |
|
59 |
-
|
60 |
|
61 |
-
|
62 |
|
63 |
-
|
64 |
|
65 |
-
|
66 |
|
67 |
- Ensure code is clean, readable, and maintainable.
|
68 |
- Adhere to proper naming conventions and consistent formatting.
|
|
|
1 |
+
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';
|
2 |
import { stripIndents } from '~/utils/stripIndent';
|
3 |
|
4 |
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
|
|
|
20 |
Use 2 spaces for code indentation
|
21 |
</code_formatting_info>
|
22 |
|
23 |
+
<diff_spec>
|
24 |
+
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:
|
25 |
+
|
26 |
+
- \`<diff path="/some/file/path.ext">\`: Contains GNU unified diff format changes
|
27 |
+
- \`<file path="/some/file/path.ext">\`: Contains the full new content of the file
|
28 |
+
|
29 |
+
The system chooses \`<file>\` if the diff exceeds the new content size, otherwise \`<diff>\`.
|
30 |
+
|
31 |
+
GNU unified diff format structure:
|
32 |
+
|
33 |
+
- For diffs the header with original and modified file names is omitted!
|
34 |
+
- Changed sections start with @@ -X,Y +A,B @@ where:
|
35 |
+
- X: Original file starting line
|
36 |
+
- Y: Original file line count
|
37 |
+
- A: Modified file starting line
|
38 |
+
- B: Modified file line count
|
39 |
+
- (-) lines: Removed from original
|
40 |
+
- (+) lines: Added in modified version
|
41 |
+
- Unmarked lines: Unchanged context
|
42 |
+
|
43 |
+
Example:
|
44 |
+
|
45 |
+
<${MODIFICATIONS_TAG_NAME}>
|
46 |
+
<diff path="/home/project/src/main.js">
|
47 |
+
@@ -2,7 +2,10 @@
|
48 |
+
return a + b;
|
49 |
+
}
|
50 |
+
|
51 |
+
-console.log('Hello, World!');
|
52 |
+
+console.log('Hello, Bolt!');
|
53 |
+
+
|
54 |
+
function greet() {
|
55 |
+
- return 'Greetings!';
|
56 |
+
+ return 'Greetings!!';
|
57 |
+
}
|
58 |
+
+
|
59 |
+
+console.log('The End');
|
60 |
+
</diff>
|
61 |
+
<file path="/home/project/package.json">
|
62 |
+
// full file content here
|
63 |
+
</file>
|
64 |
+
</${MODIFICATIONS_TAG_NAME}>
|
65 |
+
</diff_spec>
|
66 |
+
|
67 |
<artifact_info>
|
68 |
Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
|
69 |
|
|
|
72 |
- Folders to create if necessary
|
73 |
|
74 |
<artifact_instructions>
|
75 |
+
1. Think BEFORE creating an artifact.
|
76 |
+
|
77 |
+
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.
|
78 |
|
79 |
+
3. The current working directory is \`${cwd}\`.
|
80 |
|
81 |
+
4. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
|
82 |
|
83 |
+
5. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
|
84 |
|
85 |
+
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.
|
86 |
|
87 |
+
7. Use \`<boltAction>\` tags to define specific actions to perform.
|
88 |
|
89 |
+
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:
|
90 |
|
91 |
- shell: For running shell commands.
|
92 |
|
|
|
96 |
|
97 |
- 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.
|
98 |
|
99 |
+
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.
|
100 |
|
101 |
+
10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
|
102 |
|
103 |
IMPORTANT: Add all required dependencies to the \`package.json\` already and try to avoid \`npm i <pkg>\` if possible!
|
104 |
|
105 |
+
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...".
|
106 |
|
107 |
+
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!
|
108 |
|
109 |
+
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.
|
110 |
|
111 |
+
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.
|
112 |
|
113 |
- Ensure code is clean, readable, and maintainable.
|
114 |
- Adhere to proper naming conventions and consistent formatting.
|
packages/bolt/app/lib/runtime/action-runner.ts
CHANGED
@@ -37,20 +37,17 @@ export class ActionRunner {
|
|
37 |
#webcontainer: Promise<WebContainer>;
|
38 |
#currentExecutionPromise: Promise<void> = Promise.resolve();
|
39 |
|
40 |
-
actions: ActionsMap =
|
41 |
|
42 |
constructor(webcontainerPromise: Promise<WebContainer>) {
|
43 |
this.#webcontainer = webcontainerPromise;
|
44 |
-
|
45 |
-
if (import.meta.hot) {
|
46 |
-
import.meta.hot.data.actions = this.actions;
|
47 |
-
}
|
48 |
}
|
49 |
|
50 |
addAction(data: ActionCallbackData) {
|
51 |
const { actionId } = data;
|
52 |
|
53 |
-
const
|
|
|
54 |
|
55 |
if (action) {
|
56 |
// action already added
|
|
|
37 |
#webcontainer: Promise<WebContainer>;
|
38 |
#currentExecutionPromise: Promise<void> = Promise.resolve();
|
39 |
|
40 |
+
actions: ActionsMap = map({});
|
41 |
|
42 |
constructor(webcontainerPromise: Promise<WebContainer>) {
|
43 |
this.#webcontainer = webcontainerPromise;
|
|
|
|
|
|
|
|
|
44 |
}
|
45 |
|
46 |
addAction(data: ActionCallbackData) {
|
47 |
const { actionId } = data;
|
48 |
|
49 |
+
const actions = this.actions.get();
|
50 |
+
const action = actions[actionId];
|
51 |
|
52 |
if (action) {
|
53 |
// action already added
|
packages/bolt/app/lib/stores/editor.ts
CHANGED
@@ -10,7 +10,7 @@ export class EditorStore {
|
|
10 |
#filesStore: FilesStore;
|
11 |
|
12 |
selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();
|
13 |
-
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map
|
14 |
|
15 |
currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {
|
16 |
if (!selectedFile) {
|
@@ -74,7 +74,7 @@ export class EditorStore {
|
|
74 |
});
|
75 |
}
|
76 |
|
77 |
-
updateFile(filePath: string, newContent: string
|
78 |
const documents = this.documents.get();
|
79 |
const documentState = documents[filePath];
|
80 |
|
|
|
10 |
#filesStore: FilesStore;
|
11 |
|
12 |
selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();
|
13 |
+
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map({});
|
14 |
|
15 |
currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {
|
16 |
if (!selectedFile) {
|
|
|
74 |
});
|
75 |
}
|
76 |
|
77 |
+
updateFile(filePath: string, newContent: string) {
|
78 |
const documents = this.documents.get();
|
79 |
const documentState = documents[filePath];
|
80 |
|
packages/bolt/app/lib/stores/files.ts
CHANGED
@@ -1,17 +1,22 @@
|
|
1 |
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
|
|
2 |
import { map, type MapStore } from 'nanostores';
|
|
|
3 |
import * as nodePath from 'node:path';
|
4 |
import { bufferWatchEvents } from '~/utils/buffer';
|
5 |
import { WORK_DIR } from '~/utils/constants';
|
|
|
6 |
import { createScopedLogger } from '~/utils/logger';
|
|
|
7 |
|
8 |
const logger = createScopedLogger('FilesStore');
|
9 |
|
10 |
-
const
|
11 |
|
12 |
export interface File {
|
13 |
type: 'file';
|
14 |
-
content: string
|
|
|
15 |
}
|
16 |
|
17 |
export interface Folder {
|
@@ -30,6 +35,16 @@ export class FilesStore {
|
|
30 |
*/
|
31 |
#size = 0;
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});
|
34 |
|
35 |
get filesCount() {
|
@@ -41,6 +56,7 @@ export class FilesStore {
|
|
41 |
|
42 |
if (import.meta.hot) {
|
43 |
import.meta.hot.data.files = this.files;
|
|
|
44 |
}
|
45 |
|
46 |
this.#init();
|
@@ -56,7 +72,15 @@ export class FilesStore {
|
|
56 |
return dirent;
|
57 |
}
|
58 |
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
const webcontainer = await this.#webcontainer;
|
61 |
|
62 |
try {
|
@@ -66,9 +90,20 @@ export class FilesStore {
|
|
66 |
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
|
67 |
}
|
68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
await webcontainer.fs.writeFile(relativePath, content);
|
70 |
|
71 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
logger.info('File updated');
|
74 |
} catch (error) {
|
@@ -117,7 +152,21 @@ export class FilesStore {
|
|
117 |
this.#size++;
|
118 |
}
|
119 |
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
|
122 |
break;
|
123 |
}
|
@@ -140,10 +189,32 @@ export class FilesStore {
|
|
140 |
}
|
141 |
|
142 |
try {
|
143 |
-
return
|
144 |
} catch (error) {
|
145 |
console.log(error);
|
146 |
return '';
|
147 |
}
|
148 |
}
|
149 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
2 |
+
import { getEncoding } from 'istextorbinary';
|
3 |
import { map, type MapStore } from 'nanostores';
|
4 |
+
import { Buffer } from 'node:buffer';
|
5 |
import * as nodePath from 'node:path';
|
6 |
import { bufferWatchEvents } from '~/utils/buffer';
|
7 |
import { WORK_DIR } from '~/utils/constants';
|
8 |
+
import { computeFileModifications } from '~/utils/diff';
|
9 |
import { createScopedLogger } from '~/utils/logger';
|
10 |
+
import { unreachable } from '~/utils/unreachable';
|
11 |
|
12 |
const logger = createScopedLogger('FilesStore');
|
13 |
|
14 |
+
const utf8TextDecoder = new TextDecoder('utf8', { fatal: true });
|
15 |
|
16 |
export interface File {
|
17 |
type: 'file';
|
18 |
+
content: string;
|
19 |
+
isBinary: boolean;
|
20 |
}
|
21 |
|
22 |
export interface Folder {
|
|
|
35 |
*/
|
36 |
#size = 0;
|
37 |
|
38 |
+
/**
|
39 |
+
* @note Keeps track all modified files with their original content since the last user message.
|
40 |
+
* Needs to be reset when the user sends another message and all changes have to be submitted
|
41 |
+
* for the model to be aware of the changes.
|
42 |
+
*/
|
43 |
+
#modifiedFiles: Map<string, string> = import.meta.hot?.data.modifiedFiles ?? new Map();
|
44 |
+
|
45 |
+
/**
|
46 |
+
* Map of files that matches the state of WebContainer.
|
47 |
+
*/
|
48 |
files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});
|
49 |
|
50 |
get filesCount() {
|
|
|
56 |
|
57 |
if (import.meta.hot) {
|
58 |
import.meta.hot.data.files = this.files;
|
59 |
+
import.meta.hot.data.modifiedFiles = this.#modifiedFiles;
|
60 |
}
|
61 |
|
62 |
this.#init();
|
|
|
72 |
return dirent;
|
73 |
}
|
74 |
|
75 |
+
getFileModifications() {
|
76 |
+
return computeFileModifications(this.files.get(), this.#modifiedFiles);
|
77 |
+
}
|
78 |
+
|
79 |
+
resetFileModifications() {
|
80 |
+
this.#modifiedFiles.clear();
|
81 |
+
}
|
82 |
+
|
83 |
+
async saveFile(filePath: string, content: string) {
|
84 |
const webcontainer = await this.#webcontainer;
|
85 |
|
86 |
try {
|
|
|
90 |
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
|
91 |
}
|
92 |
|
93 |
+
const oldContent = this.getFile(filePath)?.content;
|
94 |
+
|
95 |
+
if (!oldContent) {
|
96 |
+
unreachable('Expected content to be defined');
|
97 |
+
}
|
98 |
+
|
99 |
await webcontainer.fs.writeFile(relativePath, content);
|
100 |
|
101 |
+
if (!this.#modifiedFiles.has(filePath)) {
|
102 |
+
this.#modifiedFiles.set(filePath, oldContent);
|
103 |
+
}
|
104 |
+
|
105 |
+
// we immediately update the file and don't rely on the `change` event coming from the watcher
|
106 |
+
this.files.setKey(filePath, { type: 'file', content, isBinary: false });
|
107 |
|
108 |
logger.info('File updated');
|
109 |
} catch (error) {
|
|
|
152 |
this.#size++;
|
153 |
}
|
154 |
|
155 |
+
let content = '';
|
156 |
+
|
157 |
+
/**
|
158 |
+
* @note This check is purely for the editor. The way we detect this is not
|
159 |
+
* bullet-proof and it's a best guess so there might be false-positives.
|
160 |
+
* The reason we do this is because we don't want to display binary files
|
161 |
+
* in the editor nor allow to edit them.
|
162 |
+
*/
|
163 |
+
const isBinary = isBinaryFile(buffer);
|
164 |
+
|
165 |
+
if (!isBinary) {
|
166 |
+
content = this.#decodeFileContent(buffer);
|
167 |
+
}
|
168 |
+
|
169 |
+
this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
|
170 |
|
171 |
break;
|
172 |
}
|
|
|
189 |
}
|
190 |
|
191 |
try {
|
192 |
+
return utf8TextDecoder.decode(buffer);
|
193 |
} catch (error) {
|
194 |
console.log(error);
|
195 |
return '';
|
196 |
}
|
197 |
}
|
198 |
}
|
199 |
+
|
200 |
+
function isBinaryFile(buffer: Uint8Array | undefined) {
|
201 |
+
if (buffer === undefined) {
|
202 |
+
return false;
|
203 |
+
}
|
204 |
+
|
205 |
+
return getEncoding(convertToBuffer(buffer), { chunkLength: 100 }) === 'binary';
|
206 |
+
}
|
207 |
+
|
208 |
+
/**
|
209 |
+
* Converts a `Uint8Array` into a Node.js `Buffer` by copying the prototype.
|
210 |
+
* The goal is to avoid expensive copies. It does create a new typed array
|
211 |
+
* but that's generally cheap as long as it uses the same underlying
|
212 |
+
* array buffer.
|
213 |
+
*/
|
214 |
+
function convertToBuffer(view: Uint8Array): Buffer {
|
215 |
+
const buffer = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
216 |
+
|
217 |
+
Object.setPrototypeOf(buffer, Buffer.prototype);
|
218 |
+
|
219 |
+
return buffer as Buffer;
|
220 |
+
}
|
packages/bolt/app/lib/stores/workbench.ts
CHANGED
@@ -70,7 +70,7 @@ export class WorkbenchStore {
|
|
70 |
this.showWorkbench.set(show);
|
71 |
}
|
72 |
|
73 |
-
setCurrentDocumentContent(newContent: string
|
74 |
const filePath = this.currentDocument.get()?.filePath;
|
75 |
|
76 |
if (!filePath) {
|
@@ -119,16 +119,15 @@ export class WorkbenchStore {
|
|
119 |
this.#editorStore.setSelectedFile(filePath);
|
120 |
}
|
121 |
|
122 |
-
async
|
123 |
-
const
|
|
|
124 |
|
125 |
-
if (
|
126 |
return;
|
127 |
}
|
128 |
|
129 |
-
|
130 |
-
|
131 |
-
await this.#filesStore.saveFile(filePath, currentDocument.value);
|
132 |
|
133 |
const newUnsavedFiles = new Set(this.unsavedFiles.get());
|
134 |
newUnsavedFiles.delete(filePath);
|
@@ -136,6 +135,16 @@ export class WorkbenchStore {
|
|
136 |
this.unsavedFiles.set(newUnsavedFiles);
|
137 |
}
|
138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
resetCurrentDocument() {
|
140 |
const currentDocument = this.currentDocument.get();
|
141 |
|
@@ -153,6 +162,20 @@ export class WorkbenchStore {
|
|
153 |
this.setCurrentDocumentContent(file.content);
|
154 |
}
|
155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
abortAllActions() {
|
157 |
// TODO: what do we wanna do and how do we wanna recover from this?
|
158 |
}
|
|
|
70 |
this.showWorkbench.set(show);
|
71 |
}
|
72 |
|
73 |
+
setCurrentDocumentContent(newContent: string) {
|
74 |
const filePath = this.currentDocument.get()?.filePath;
|
75 |
|
76 |
if (!filePath) {
|
|
|
119 |
this.#editorStore.setSelectedFile(filePath);
|
120 |
}
|
121 |
|
122 |
+
async saveFile(filePath: string) {
|
123 |
+
const documents = this.#editorStore.documents.get();
|
124 |
+
const document = documents[filePath];
|
125 |
|
126 |
+
if (document === undefined) {
|
127 |
return;
|
128 |
}
|
129 |
|
130 |
+
await this.#filesStore.saveFile(filePath, document.value);
|
|
|
|
|
131 |
|
132 |
const newUnsavedFiles = new Set(this.unsavedFiles.get());
|
133 |
newUnsavedFiles.delete(filePath);
|
|
|
135 |
this.unsavedFiles.set(newUnsavedFiles);
|
136 |
}
|
137 |
|
138 |
+
async saveCurrentDocument() {
|
139 |
+
const currentDocument = this.currentDocument.get();
|
140 |
+
|
141 |
+
if (currentDocument === undefined) {
|
142 |
+
return;
|
143 |
+
}
|
144 |
+
|
145 |
+
await this.saveFile(currentDocument.filePath);
|
146 |
+
}
|
147 |
+
|
148 |
resetCurrentDocument() {
|
149 |
const currentDocument = this.currentDocument.get();
|
150 |
|
|
|
162 |
this.setCurrentDocumentContent(file.content);
|
163 |
}
|
164 |
|
165 |
+
async saveAllFiles() {
|
166 |
+
for (const filePath of this.unsavedFiles.get()) {
|
167 |
+
await this.saveFile(filePath);
|
168 |
+
}
|
169 |
+
}
|
170 |
+
|
171 |
+
getFileModifcations() {
|
172 |
+
return this.#filesStore.getFileModifications();
|
173 |
+
}
|
174 |
+
|
175 |
+
resetAllFileModifications() {
|
176 |
+
this.#filesStore.resetFileModifications();
|
177 |
+
}
|
178 |
+
|
179 |
abortAllActions() {
|
180 |
// TODO: what do we wanna do and how do we wanna recover from this?
|
181 |
}
|
packages/bolt/app/routes/api.chat.ts
CHANGED
@@ -7,6 +7,7 @@ import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
|
7 |
|
8 |
export async function action({ context, request }: ActionFunctionArgs) {
|
9 |
const { messages } = await request.json<{ messages: Messages }>();
|
|
|
10 |
const stream = new SwitchableStream();
|
11 |
|
12 |
try {
|
|
|
7 |
|
8 |
export async function action({ context, request }: ActionFunctionArgs) {
|
9 |
const { messages } = await request.json<{ messages: Messages }>();
|
10 |
+
|
11 |
const stream = new SwitchableStream();
|
12 |
|
13 |
try {
|
packages/bolt/app/utils/constants.ts
CHANGED
@@ -1,2 +1,3 @@
|
|
1 |
export const WORK_DIR_NAME = 'project';
|
2 |
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
|
|
|
1 |
export const WORK_DIR_NAME = 'project';
|
2 |
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
3 |
+
export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
|
packages/bolt/app/utils/diff.ts
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createTwoFilesPatch } from 'diff';
|
2 |
+
import type { FileMap } from '~/lib/stores/files';
|
3 |
+
import { MODIFICATIONS_TAG_NAME } from './constants';
|
4 |
+
|
5 |
+
export const modificationsRegex = new RegExp(
|
6 |
+
`^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
|
7 |
+
'g',
|
8 |
+
);
|
9 |
+
|
10 |
+
interface ModifiedFile {
|
11 |
+
type: 'diff' | 'file';
|
12 |
+
content: string;
|
13 |
+
}
|
14 |
+
|
15 |
+
type FileModifications = Record<string, ModifiedFile>;
|
16 |
+
|
17 |
+
export function computeFileModifications(files: FileMap, modifiedFiles: Map<string, string>) {
|
18 |
+
const modifications: FileModifications = {};
|
19 |
+
|
20 |
+
let hasModifiedFiles = false;
|
21 |
+
|
22 |
+
for (const [filePath, originalContent] of modifiedFiles) {
|
23 |
+
const file = files[filePath];
|
24 |
+
|
25 |
+
if (file?.type !== 'file') {
|
26 |
+
continue;
|
27 |
+
}
|
28 |
+
|
29 |
+
const unifiedDiff = diffFiles(filePath, originalContent, file.content);
|
30 |
+
|
31 |
+
if (!unifiedDiff) {
|
32 |
+
// files are identical
|
33 |
+
continue;
|
34 |
+
}
|
35 |
+
|
36 |
+
hasModifiedFiles = true;
|
37 |
+
|
38 |
+
if (unifiedDiff.length > file.content.length) {
|
39 |
+
// if there are lots of changes we simply grab the current file content since it's smaller than the diff
|
40 |
+
modifications[filePath] = { type: 'file', content: file.content };
|
41 |
+
} else {
|
42 |
+
// otherwise we use the diff since it's smaller
|
43 |
+
modifications[filePath] = { type: 'diff', content: unifiedDiff };
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
if (!hasModifiedFiles) {
|
48 |
+
return undefined;
|
49 |
+
}
|
50 |
+
|
51 |
+
return modifications;
|
52 |
+
}
|
53 |
+
|
54 |
+
/**
|
55 |
+
* Computes a diff in the unified format. The only difference is that the header is omitted
|
56 |
+
* because it will always assume that you're comparing two versions of the same file and
|
57 |
+
* it allows us to avoid the extra characters we send back to the llm.
|
58 |
+
*
|
59 |
+
* @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html
|
60 |
+
*/
|
61 |
+
export function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) {
|
62 |
+
let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent);
|
63 |
+
|
64 |
+
const patchHeaderEnd = `--- ${fileName}\n+++ ${fileName}\n`;
|
65 |
+
const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd);
|
66 |
+
|
67 |
+
if (headerEndIndex >= 0) {
|
68 |
+
unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length);
|
69 |
+
}
|
70 |
+
|
71 |
+
if (unifiedDiff === '') {
|
72 |
+
return undefined;
|
73 |
+
}
|
74 |
+
|
75 |
+
return unifiedDiff;
|
76 |
+
}
|
77 |
+
|
78 |
+
/**
|
79 |
+
* Converts the unified diff to HTML.
|
80 |
+
*
|
81 |
+
* Example:
|
82 |
+
*
|
83 |
+
* ```html
|
84 |
+
* <bolt_file_modifications>
|
85 |
+
* <diff path="/home/project/index.js">
|
86 |
+
* - console.log('Hello, World!');
|
87 |
+
* + console.log('Hello, Bolt!');
|
88 |
+
* </diff>
|
89 |
+
* </bolt_file_modifications>
|
90 |
+
* ```
|
91 |
+
*/
|
92 |
+
export function fileModificationsToHTML(modifications: FileModifications) {
|
93 |
+
const entries = Object.entries(modifications);
|
94 |
+
|
95 |
+
if (entries.length === 0) {
|
96 |
+
return undefined;
|
97 |
+
}
|
98 |
+
|
99 |
+
const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`];
|
100 |
+
|
101 |
+
for (const [filePath, { type, content }] of entries) {
|
102 |
+
result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `</${type}>`);
|
103 |
+
}
|
104 |
+
|
105 |
+
result.push(`</${MODIFICATIONS_TAG_NAME}>`);
|
106 |
+
|
107 |
+
return result.join('\n');
|
108 |
+
}
|
packages/bolt/package.json
CHANGED
@@ -43,8 +43,10 @@
|
|
43 |
"@xterm/addon-web-links": "^0.11.0",
|
44 |
"@xterm/xterm": "^5.5.0",
|
45 |
"ai": "^3.2.27",
|
|
|
46 |
"framer-motion": "^11.2.12",
|
47 |
"isbot": "^4.1.0",
|
|
|
48 |
"nanostores": "^0.10.3",
|
49 |
"react": "^18.2.0",
|
50 |
"react-dom": "^18.2.0",
|
@@ -59,6 +61,7 @@
|
|
59 |
"devDependencies": {
|
60 |
"@cloudflare/workers-types": "^4.20240620.0",
|
61 |
"@remix-run/dev": "^2.10.0",
|
|
|
62 |
"@types/react": "^18.2.20",
|
63 |
"@types/react-dom": "^18.2.7",
|
64 |
"fast-glob": "^3.3.2",
|
|
|
43 |
"@xterm/addon-web-links": "^0.11.0",
|
44 |
"@xterm/xterm": "^5.5.0",
|
45 |
"ai": "^3.2.27",
|
46 |
+
"diff": "^5.2.0",
|
47 |
"framer-motion": "^11.2.12",
|
48 |
"isbot": "^4.1.0",
|
49 |
+
"istextorbinary": "^9.5.0",
|
50 |
"nanostores": "^0.10.3",
|
51 |
"react": "^18.2.0",
|
52 |
"react-dom": "^18.2.0",
|
|
|
61 |
"devDependencies": {
|
62 |
"@cloudflare/workers-types": "^4.20240620.0",
|
63 |
"@remix-run/dev": "^2.10.0",
|
64 |
+
"@types/diff": "^5.2.1",
|
65 |
"@types/react": "^18.2.20",
|
66 |
"@types/react-dom": "^18.2.7",
|
67 |
"fast-glob": "^3.3.2",
|
packages/bolt/types/istextorbinary.d.ts
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* @note For some reason the types aren't picked up from node_modules so I declared the module here
|
3 |
+
* with only the function that we use.
|
4 |
+
*/
|
5 |
+
declare module 'istextorbinary' {
|
6 |
+
export interface EncodingOpts {
|
7 |
+
/** Defaults to 24 */
|
8 |
+
chunkLength?: number;
|
9 |
+
|
10 |
+
/** If not provided, will check the start, beginning, and end */
|
11 |
+
chunkBegin?: number;
|
12 |
+
}
|
13 |
+
|
14 |
+
export function getEncoding(buffer: Buffer | null, opts?: EncodingOpts): 'utf8' | 'binary' | null;
|
15 |
+
}
|
packages/bolt/vite.config.ts
CHANGED
@@ -12,7 +12,7 @@ export default defineConfig((config) => {
|
|
12 |
},
|
13 |
plugins: [
|
14 |
nodePolyfills({
|
15 |
-
include: ['path'],
|
16 |
}),
|
17 |
config.mode !== 'test' && remixCloudflareDevProxy(),
|
18 |
remixVitePlugin({
|
|
|
12 |
},
|
13 |
plugins: [
|
14 |
nodePolyfills({
|
15 |
+
include: ['path', 'buffer'],
|
16 |
}),
|
17 |
config.mode !== 'test' && remixCloudflareDevProxy(),
|
18 |
remixVitePlugin({
|
pnpm-lock.yaml
CHANGED
@@ -116,12 +116,18 @@ importers:
|
|
116 |
ai:
|
117 |
specifier: ^3.2.27
|
118 |
version: 3.2.27([email protected])([email protected])([email protected]([email protected]))([email protected])
|
|
|
|
|
|
|
119 |
framer-motion:
|
120 |
specifier: ^11.2.12
|
121 |
version: 11.2.12([email protected]([email protected]))([email protected])
|
122 |
isbot:
|
123 |
specifier: ^4.1.0
|
124 |
version: 4.4.0
|
|
|
|
|
|
|
125 |
nanostores:
|
126 |
specifier: ^0.10.3
|
127 |
version: 0.10.3
|
@@ -159,6 +165,9 @@ importers:
|
|
159 |
'@remix-run/dev':
|
160 |
specifier: ^2.10.0
|
161 |
version: 2.10.0(@remix-run/[email protected]([email protected]([email protected]))([email protected])([email protected]))(@remix-run/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected]))([email protected](@cloudflare/[email protected]))
|
|
|
|
|
|
|
162 |
'@types/react':
|
163 |
specifier: ^18.2.20
|
164 |
version: 18.3.3
|
@@ -1445,6 +1454,9 @@ packages:
|
|
1445 |
'@types/[email protected]':
|
1446 |
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
1447 |
|
|
|
|
|
|
|
1448 |
'@types/[email protected]':
|
1449 |
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
1450 |
|
@@ -1865,6 +1877,10 @@ packages:
|
|
1865 |
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
1866 |
engines: {node: '>=8'}
|
1867 |
|
|
|
|
|
|
|
|
|
1868 | |
1869 |
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
1870 |
|
@@ -2328,6 +2344,10 @@ packages:
|
|
2328 | |
2329 |
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
2330 |
|
|
|
|
|
|
|
|
|
2331 | |
2332 |
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
2333 |
|
@@ -3040,6 +3060,10 @@ packages:
|
|
3040 |
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
|
3041 |
engines: {node: '>=10'}
|
3042 |
|
|
|
|
|
|
|
|
|
3043 | |
3044 |
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
|
3045 |
engines: {node: '>=14'}
|
@@ -4522,6 +4546,10 @@ packages:
|
|
4522 | |
4523 |
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
4524 |
|
|
|
|
|
|
|
|
|
4525 | |
4526 |
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
4527 |
|
@@ -4766,6 +4794,10 @@ packages:
|
|
4766 |
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
4767 |
engines: {node: '>= 0.8'}
|
4768 |
|
|
|
|
|
|
|
|
|
4769 | |
4770 |
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
|
4771 |
|
@@ -6344,6 +6376,8 @@ snapshots:
|
|
6344 |
|
6345 |
'@types/[email protected]': {}
|
6346 |
|
|
|
|
|
6347 |
'@types/[email protected]':
|
6348 |
dependencies:
|
6349 |
'@types/estree': 1.0.5
|
@@ -6933,6 +6967,10 @@ snapshots:
|
|
6933 |
|
6934 | |
6935 |
|
|
|
|
|
|
|
|
|
6936 | |
6937 |
dependencies:
|
6938 |
buffer: 5.7.1
|
@@ -7435,6 +7473,10 @@ snapshots:
|
|
7435 |
|
7436 | |
7437 |
|
|
|
|
|
|
|
|
|
7438 | |
7439 |
|
7440 | |
@@ -8284,6 +8326,12 @@ snapshots:
|
|
8284 |
|
8285 | |
8286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
8287 | |
8288 |
dependencies:
|
8289 |
'@isaacs/cliui': 8.0.2
|
@@ -10228,6 +10276,10 @@ snapshots:
|
|
10228 |
|
10229 | |
10230 |
|
|
|
|
|
|
|
|
|
10231 | |
10232 |
dependencies:
|
10233 |
readable-stream: 2.3.8
|
@@ -10502,6 +10554,8 @@ snapshots:
|
|
10502 |
|
10503 | |
10504 |
|
|
|
|
|
10505 | |
10506 |
dependencies:
|
10507 |
'@types/unist': 3.0.2
|
|
|
116 |
ai:
|
117 |
specifier: ^3.2.27
|
118 |
version: 3.2.27([email protected])([email protected])([email protected]([email protected]))([email protected])
|
119 |
+
diff:
|
120 |
+
specifier: ^5.2.0
|
121 |
+
version: 5.2.0
|
122 |
framer-motion:
|
123 |
specifier: ^11.2.12
|
124 |
version: 11.2.12([email protected]([email protected]))([email protected])
|
125 |
isbot:
|
126 |
specifier: ^4.1.0
|
127 |
version: 4.4.0
|
128 |
+
istextorbinary:
|
129 |
+
specifier: ^9.5.0
|
130 |
+
version: 9.5.0
|
131 |
nanostores:
|
132 |
specifier: ^0.10.3
|
133 |
version: 0.10.3
|
|
|
165 |
'@remix-run/dev':
|
166 |
specifier: ^2.10.0
|
167 |
version: 2.10.0(@remix-run/[email protected]([email protected]([email protected]))([email protected])([email protected]))(@remix-run/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected]))([email protected](@cloudflare/[email protected]))
|
168 |
+
'@types/diff':
|
169 |
+
specifier: ^5.2.1
|
170 |
+
version: 5.2.1
|
171 |
'@types/react':
|
172 |
specifier: ^18.2.20
|
173 |
version: 18.3.3
|
|
|
1454 |
'@types/[email protected]':
|
1455 |
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
1456 |
|
1457 |
+
'@types/[email protected]':
|
1458 |
+
resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==}
|
1459 |
+
|
1460 |
'@types/[email protected]':
|
1461 |
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
1462 |
|
|
|
1877 |
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
1878 |
engines: {node: '>=8'}
|
1879 |
|
1880 | |
1881 |
+
resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==}
|
1882 |
+
engines: {node: '>=4'}
|
1883 |
+
|
1884 | |
1885 |
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
1886 |
|
|
|
2344 | |
2345 |
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
2346 |
|
2347 | |
2348 |
+
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
2349 |
+
engines: {node: '>=4'}
|
2350 |
+
|
2351 | |
2352 |
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
2353 |
|
|
|
3060 |
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
|
3061 |
engines: {node: '>=10'}
|
3062 |
|
3063 | |
3064 |
+
resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==}
|
3065 |
+
engines: {node: '>=4'}
|
3066 |
+
|
3067 | |
3068 |
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
|
3069 |
engines: {node: '>=14'}
|
|
|
4546 | |
4547 |
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
4548 |
|
4549 | |
4550 |
+
resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
|
4551 |
+
engines: {node: '>=4'}
|
4552 |
+
|
4553 | |
4554 |
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
4555 |
|
|
|
4794 |
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
4795 |
engines: {node: '>= 0.8'}
|
4796 |
|
4797 | |
4798 |
+
resolution: {integrity: sha512-gjb0ARm9qlcBAonU4zPwkl9ecKkas+tC2CGwFfptTCWWIVTWY1YUbT2zZKsOAF1jR/tNxxyLwwG0cb42XlYcTg==}
|
4799 |
+
engines: {node: '>=4'}
|
4800 |
+
|
4801 | |
4802 |
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
|
4803 |
|
|
|
6376 |
|
6377 |
'@types/[email protected]': {}
|
6378 |
|
6379 |
+
'@types/[email protected]': {}
|
6380 |
+
|
6381 |
'@types/[email protected]':
|
6382 |
dependencies:
|
6383 |
'@types/estree': 1.0.5
|
|
|
6967 |
|
6968 | |
6969 |
|
6970 | |
6971 |
+
dependencies:
|
6972 |
+
editions: 6.21.0
|
6973 |
+
|
6974 | |
6975 |
dependencies:
|
6976 |
buffer: 5.7.1
|
|
|
7473 |
|
7474 | |
7475 |
|
7476 | |
7477 |
+
dependencies:
|
7478 |
+
version-range: 4.14.0
|
7479 |
+
|
7480 | |
7481 |
|
7482 | |
|
|
8326 |
|
8327 | |
8328 |
|
8329 | |
8330 |
+
dependencies:
|
8331 |
+
binaryextensions: 6.11.0
|
8332 |
+
editions: 6.21.0
|
8333 |
+
textextensions: 6.11.0
|
8334 |
+
|
8335 | |
8336 |
dependencies:
|
8337 |
'@isaacs/cliui': 8.0.2
|
|
|
10276 |
|
10277 | |
10278 |
|
10279 | |
10280 |
+
dependencies:
|
10281 |
+
editions: 6.21.0
|
10282 |
+
|
10283 | |
10284 |
dependencies:
|
10285 |
readable-stream: 2.3.8
|
|
|
10554 |
|
10555 | |
10556 |
|
10557 |
+
[email protected]: {}
|
10558 |
+
|
10559 | |
10560 |
dependencies:
|
10561 |
'@types/unist': 3.0.2
|