feat: add first version of workbench, increase token limit, improve system prompt

This commit is contained in:
Dominic Elm
2024-07-17 20:54:46 +02:00
parent b4420a22bb
commit 621b8804d8
50 changed files with 2979 additions and 423 deletions

View File

@@ -0,0 +1,2 @@
// see https://docs.anthropic.com/en/docs/about-claude/models
export const MAX_TOKENS = 8192;

View File

@@ -1,4 +1,4 @@
export const systemPrompt = `
export const getSystemPrompt = (cwd: string = '/home/project') => `
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
<system_constraints>
@@ -22,34 +22,45 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
- Shell commands to run including dependencies to install using a package manager (NPM)
- Files to create and their contents
- Folders to create if necessary
<artifact_instructions>
1. Think BEFORE creating an artifact.
1. Think BEFORE creating an artifact
2. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
2. The current working directory is \`${cwd}\`.
3. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
3. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
3. Use \`<boltAction>\` tags to define specific actions to perform.
4. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
4. 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:
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.
- shell: For running shell commands. When Using \`npx\`, ALWAYS provide the \`--yes\` flag!
6. Use \`<boltAction>\` tags to define specific actions to perform.
- file: For writing new files or updating existing files. For each file add a \`path\` attribute to the opening \`<boltArtifact>\` tag to specify the file path. The content of the the file artifact is the file contents.
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:
4. 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.
- shell: For running shell commands.
5. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
- When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
- When running multiple shell commands, use \`&&\` to run them sequentially.
- Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed. If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.
- 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. 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!
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
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...".
6. 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!
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!
</artifact_instructions>
</artifact_info>
BEST PRACTICES: Follow coding best practices. Whenever creating files, split functionality into smaller modules instead of placing everything in a single large file. If possible, files should be as small as possible, with functionality extracted into separate modules. This is VERY IMPORTANT so that updates to the project can be done to smaller modules without re-generating large files!
NEVER use the word "artifact". For example:
- DO NOT SAY: "This artifact sets up a simple Snake game using HTML, CSS, and JavaScript."
- INSTEAD SAY: "We set up a simple Snake game using HTML, CSS, and JavaScript."
@@ -69,16 +80,10 @@ Here are some examples of correct usage of artifacts:
<assistant_response>
Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
<boltArtifact title="JavaScript Factorial Function">
<boltAction type="file" path="index.js">
<boltArtifact id="factorial-function" title="JavaScript Factorial Function">
<boltAction type="file" filePath="index.js">
function factorial(n) {
if (n === 0 || n === 1) {
return 1;
} else if (n < 0) {
return "Factorial is not defined for negative numbers";
} else {
return n * factorial(n - 1);
}
...
}
...
@@ -97,8 +102,8 @@ Here are some examples of correct usage of artifacts:
<assistant_response>
Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
<boltArtifact title="Snake Game in HTML and JavaScript">
<boltAction type="file" path="package.json">
<boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
<boltAction type="file" filePath="package.json">
{
"name": "snake",
"scripts": {
@@ -112,7 +117,7 @@ Here are some examples of correct usage of artifacts:
npm install --save-dev vite
</boltAction>
<boltAction type="file" path="index.html">
<boltAction type="file" filePath="index.html">
...
</boltAction>
@@ -131,8 +136,8 @@ Here are some examples of correct usage of artifacts:
<assistant_response>
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
<boltArtifact title="Bouncing Ball with Gravity in React">
<boltAction type="file" path="package.json">
<boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
<boltAction type="file" filePath="package.json">
{
"name": "bouncing-ball",
"private": true,
@@ -157,19 +162,19 @@ Here are some examples of correct usage of artifacts:
}
</boltAction>
<boltAction type="file" path="index.html">
<boltAction type="file" filePath="index.html">
...
</boltAction>
<boltAction type="file" path="src/main.jsx">
<boltAction type="file" filePath="src/main.jsx">
...
</boltAction>
<boltAction type="file" path="src/index.css">
<boltAction type="file" filePath="src/index.css">
...
</boltAction>
<boltAction type="file" path="src/App.jsx">
<boltAction type="file" filePath="src/App.jsx">
...
</boltAction>

View File

@@ -0,0 +1,38 @@
import { streamText as _streamText, convertToCoreMessages } from 'ai';
import { getAPIKey } from '../llm/api-key';
import { getAnthropicModel } from '../llm/model';
import { MAX_TOKENS } from './constants';
import { getSystemPrompt } from './prompts';
interface ToolResult<Name extends string, Args, Result> {
toolCallId: string;
toolName: Name;
args: Args;
result: Result;
}
interface Message {
role: 'user' | 'assistant';
content: string;
toolInvocations?: ToolResult<string, unknown, unknown>[];
}
export type Messages = Message[];
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
return _streamText({
model: getAnthropicModel(getAPIKey(env)),
system: getSystemPrompt(),
maxTokens: MAX_TOKENS,
headers: {
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
},
messages: convertToCoreMessages(messages),
onFinish: ({ finishReason, usage, warnings }) => {
console.log({ finishReason, usage, warnings });
},
...options,
});
}

View File

@@ -1,23 +1,28 @@
import type { Message } from 'ai';
import { useCallback, useState } from 'react';
import { StreamingMessageParser } from '~/lib/runtime/message-parser';
import { workspaceStore } from '~/lib/stores/workspace';
import { createScopedLogger } from '~/utils/logger';
import { createScopedLogger } from '../../utils/logger';
import { StreamingMessageParser } from '../runtime/message-parser';
import { workbenchStore } from '../stores/workbench';
const logger = createScopedLogger('useMessageParser');
const messageParser = new StreamingMessageParser({
callbacks: {
onArtifactOpen: (messageId, { title }) => {
logger.debug('onArtifactOpen', title);
workspaceStore.updateArtifact(messageId, { title, closed: false });
onArtifactOpen: (data) => {
logger.trace('onArtifactOpen', data);
workbenchStore.showWorkbench.set(true);
workbenchStore.addArtifact(data);
},
onArtifactClose: (messageId) => {
logger.debug('onArtifactClose');
workspaceStore.updateArtifact(messageId, { closed: true });
onArtifactClose: (data) => {
logger.trace('onArtifactClose');
workbenchStore.updateArtifact(data, { closed: true });
},
onAction: (messageId, { type, path, content }) => {
console.log('ACTION', messageId, { type, path, content });
onAction: (data) => {
logger.trace('onAction', data);
workbenchStore.runAction(data);
},
},
});

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { createScopedLogger } from '~/utils/logger';
import { createScopedLogger } from '../../utils/logger';
const logger = createScopedLogger('usePromptEnhancement');

View File

@@ -0,0 +1,68 @@
import { WebContainer } from '@webcontainer/api';
import * as nodePath from 'node:path';
import { createScopedLogger } from '../../utils/logger';
import type { ActionCallbackData } from './message-parser';
const logger = createScopedLogger('ActionRunner');
export class ActionRunner {
#webcontainer: Promise<WebContainer>;
constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise;
}
async runAction({ action }: ActionCallbackData, abortSignal?: AbortSignal) {
logger.trace('Running action', action);
const { content } = action;
const webcontainer = await this.#webcontainer;
switch (action.type) {
case 'file': {
let folder = nodePath.dirname(action.filePath);
// remove trailing slashes
folder = folder.replace(/\/$/g, '');
if (folder !== '.') {
try {
await webcontainer.fs.mkdir(folder, { recursive: true });
logger.debug('Created folder', folder);
} catch (error) {
logger.error('Failed to create folder\n', error);
}
}
try {
await webcontainer.fs.writeFile(action.filePath, content);
logger.debug(`File written ${action.filePath}`);
} catch (error) {
logger.error('Failed to write file\n', error);
}
break;
}
case 'shell': {
const process = await webcontainer.spawn('jsh', ['-c', content]);
abortSignal?.addEventListener('abort', () => {
process.kill();
});
process.output.pipeTo(
new WritableStream({
write(data) {
console.log(data);
},
}),
);
const exitCode = await process.exit;
logger.debug(`Process terminated with code ${exitCode}`);
}
}
}
}

View File

@@ -38,28 +38,30 @@ describe('StreamingMessageParser', () => {
['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'],
['Before <boltArtifact title="Some title">foo</boltArtifact> After', 'Before After'],
[
'Before <boltArtifact title="Some title"><boltAction type="shell">npm install</boltAction></boltArtifact> After',
'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction></boltArtifact> After',
'Before After',
[{ type: 'shell', content: 'npm install' }],
],
[
'Before <boltArtifact title="Some title"><boltAction type="shell">npm install</boltAction><boltAction type="file" path="index.js">some content</boltAction></boltArtifact> After',
'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction><boltAction type="file" filePath="index.js">some content</boltAction></boltArtifact> After',
'Before After',
[
{ type: 'shell', content: 'npm install' },
{ type: 'file', path: 'index.js', content: 'some content\n' },
{ type: 'file', filePath: 'index.js', content: 'some content\n' },
],
],
])('should correctly parse chunks and strip out bolt artifacts', (input, expected, expectedActions = []) => {
let actionCounter = 0;
const testId = 'test_id';
const expectedArtifactId = 'artifact_1';
const expectedMessageId = 'message_1';
const parser = new StreamingMessageParser({
artifactElement: '',
callbacks: {
onAction: (id, action) => {
expect(testId).toBe(id);
onAction: ({ artifactId, messageId, action }) => {
expect(artifactId).toBe(expectedArtifactId);
expect(messageId).toBe(expectedMessageId);
expect(action).toEqual(expectedActions[actionCounter]);
actionCounter++;
},
@@ -75,7 +77,7 @@ describe('StreamingMessageParser', () => {
for (const chunk of chunks) {
message += chunk;
result += parser.parse(testId, message);
result += parser.parse(expectedMessageId, message);
}
expect(actionCounter).toBe(expectedActions.length);

View File

@@ -1,24 +1,30 @@
import type { ActionType, BoltAction, BoltActionData, FileAction, ShellAction } from '../../types/actions';
import type { BoltArtifactData } from '../../types/artifact';
import { createScopedLogger } from '../../utils/logger';
import { unreachable } from '../../utils/unreachable';
const ARTIFACT_TAG_OPEN = '<boltArtifact';
const ARTIFACT_TAG_CLOSE = '</boltArtifact>';
const ARTIFACT_ACTION_TAG_OPEN = '<boltAction';
const ARTIFACT_ACTION_TAG_CLOSE = '</boltAction>';
interface BoltArtifact {
title: string;
const logger = createScopedLogger('MessageParser');
export interface ArtifactCallbackData extends BoltArtifactData {
messageId: string;
}
type ArtifactOpenCallback = (messageId: string, artifact: BoltArtifact) => void;
type ArtifactCloseCallback = (messageId: string) => void;
type ActionCallback = (messageId: string, action: BoltActionData) => void;
type ActionType = 'file' | 'shell';
export interface BoltActionData {
type?: ActionType;
path?: string;
content: string;
export interface ActionCallbackData {
artifactId: string;
messageId: string;
actionId: string;
action: BoltAction;
}
type ArtifactOpenCallback = (data: ArtifactCallbackData) => void;
type ArtifactCloseCallback = (data: ArtifactCallbackData) => void;
type ActionCallback = (data: ActionCallbackData) => void;
interface Callbacks {
onArtifactOpen?: ArtifactOpenCallback;
onArtifactClose?: ArtifactCloseCallback;
@@ -32,39 +38,72 @@ interface StreamingMessageParserOptions {
artifactElement?: string | ElementFactory;
}
interface MessageState {
position: number;
insideArtifact: boolean;
insideAction: boolean;
currentArtifact?: BoltArtifactData;
currentAction: BoltActionData;
actionId: number;
}
export class StreamingMessageParser {
#lastPositions = new Map<string, number>();
#insideArtifact = false;
#insideAction = false;
#currentAction: BoltActionData = { content: '' };
#messages = new Map<string, MessageState>();
constructor(private _options: StreamingMessageParserOptions = {}) {}
parse(id: string, input: string) {
parse(messageId: string, input: string) {
let state = this.#messages.get(messageId);
if (!state) {
state = {
position: 0,
insideAction: false,
insideArtifact: false,
currentAction: { content: '' },
actionId: 0,
};
this.#messages.set(messageId, state);
}
let output = '';
let i = this.#lastPositions.get(id) ?? 0;
let i = state.position;
let earlyBreak = false;
while (i < input.length) {
if (this.#insideArtifact) {
if (this.#insideAction) {
if (state.insideArtifact) {
const currentArtifact = state.currentArtifact;
if (currentArtifact === undefined) {
unreachable('Artifact not initialized');
}
if (state.insideAction) {
const closeIndex = input.indexOf(ARTIFACT_ACTION_TAG_CLOSE, i);
const currentAction = state.currentAction;
if (closeIndex !== -1) {
this.#currentAction.content += input.slice(i, closeIndex);
currentAction.content += input.slice(i, closeIndex);
let content = this.#currentAction.content.trim();
let content = currentAction.content.trim();
if (this.#currentAction.type === 'file') {
if ('type' in currentAction && currentAction.type === 'file') {
content += '\n';
}
this.#currentAction.content = content;
currentAction.content = content;
this._options.callbacks?.onAction?.(id, this.#currentAction);
this._options.callbacks?.onAction?.({
artifactId: currentArtifact.id,
messageId,
actionId: String(state.actionId++),
action: currentAction as BoltAction,
});
this.#insideAction = false;
this.#currentAction = { content: '' };
state.insideAction = false;
state.currentAction = { content: '' };
i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
} else {
@@ -79,17 +118,39 @@ export class StreamingMessageParser {
if (actionEndIndex !== -1) {
const actionTag = input.slice(actionOpenIndex, actionEndIndex + 1);
this.#currentAction.type = this.#extractAttribute(actionTag, 'type') as ActionType;
this.#currentAction.path = this.#extractAttribute(actionTag, 'path');
this.#insideAction = true;
const actionType = this.#extractAttribute(actionTag, 'type') as ActionType;
const actionAttributes = {
type: actionType,
content: '',
};
if (actionType === 'file') {
const filePath = this.#extractAttribute(actionTag, 'filePath') as string;
if (!filePath) {
logger.debug('File path not specified');
}
(actionAttributes as FileAction).filePath = filePath;
} else if (actionType !== 'shell') {
logger.warn(`Unknown action type '${actionType}'`);
}
state.currentAction = actionAttributes as FileAction | ShellAction;
state.insideAction = true;
i = actionEndIndex + 1;
} else {
break;
}
} else if (artifactCloseIndex !== -1) {
this.#insideArtifact = false;
this._options.callbacks?.onArtifactClose?.({ messageId, ...currentArtifact });
this._options.callbacks?.onArtifactClose?.(id);
state.insideArtifact = false;
state.currentArtifact = undefined;
i = artifactCloseIndex + ARTIFACT_TAG_CLOSE.length;
} else {
@@ -118,12 +179,30 @@ export class StreamingMessageParser {
const artifactTag = input.slice(i, openTagEnd + 1);
const artifactTitle = this.#extractAttribute(artifactTag, 'title') as string;
const artifactId = this.#extractAttribute(artifactTag, 'id') as string;
this.#insideArtifact = true;
if (!artifactTitle) {
logger.warn('Artifact title missing');
}
this._options.callbacks?.onArtifactOpen?.(id, { title: artifactTitle });
if (!artifactId) {
logger.warn('Artifact id missing');
}
output += this._options.artifactElement ?? `<div class="__boltArtifact__" data-message-id="${id}"></div>`;
state.insideArtifact = true;
const currentArtifact = {
id: artifactId,
title: artifactTitle,
} satisfies BoltArtifactData;
state.currentArtifact = currentArtifact;
this._options.callbacks?.onArtifactOpen?.({ messageId, ...currentArtifact });
output +=
this._options.artifactElement ??
`<div class="__boltArtifact__" data-artifact-id="${artifactId}" data-message-id="${messageId}"></div>`;
i = openTagEnd + 1;
} else {
@@ -153,16 +232,13 @@ export class StreamingMessageParser {
}
}
this.#lastPositions.set(id, i);
state.position = i;
return output;
}
reset() {
this.#lastPositions.clear();
this.#insideArtifact = false;
this.#insideAction = false;
this.#currentAction = { content: '' };
this.#messages.clear();
}
#extractAttribute(tag: string, attributeName: string): string | undefined {

View File

@@ -0,0 +1,42 @@
import type { WebContainer } from '@webcontainer/api';
import { atom } from 'nanostores';
export interface PreviewInfo {
port: number;
ready: boolean;
baseUrl: string;
}
export class PreviewsStore {
#availablePreviews = new Map<number, PreviewInfo>();
#webcontainer: Promise<WebContainer>;
previews = atom<PreviewInfo[]>([]);
constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise;
this.#init();
}
async #init() {
const webcontainer = await this.#webcontainer;
webcontainer.on('port', (port, type, url) => {
let previewInfo = this.#availablePreviews.get(port);
const previews = this.previews.get();
if (!previewInfo) {
previewInfo = { port, ready: type === 'open', baseUrl: url };
this.#availablePreviews.set(port, previewInfo);
previews.push(previewInfo);
}
previewInfo.ready = type === 'open';
previewInfo.baseUrl = url;
this.previews.set([...previews]);
});
}
}

View File

@@ -0,0 +1,33 @@
import { atom } from 'nanostores';
export type Theme = 'dark' | 'light';
export const kTheme = 'bolt_theme';
export function themeIsDark() {
return themeStore.get() === 'dark';
}
export const themeStore = atom<Theme>(initStore());
function initStore() {
if (!import.meta.env.SSR) {
const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined;
const themeAttribute = document.querySelector('html')?.getAttribute('data-theme');
return persistedTheme ?? (themeAttribute as Theme) ?? 'light';
}
return 'light';
}
export function toggleTheme() {
const currentTheme = themeStore.get();
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
themeStore.set(newTheme);
localStorage.setItem(kTheme, newTheme);
document.querySelector('html')?.setAttribute('data-theme', newTheme);
}

View File

@@ -0,0 +1,153 @@
import { atom, map, type MapStore, type WritableAtom } from 'nanostores';
import type { BoltAction } from '../../types/actions';
import { unreachable } from '../../utils/unreachable';
import { ActionRunner } from '../runtime/action-runner';
import type { ActionCallbackData, ArtifactCallbackData } from '../runtime/message-parser';
import { webcontainer } from '../webcontainer';
import { PreviewsStore } from './previews';
export type RunningState = BoltAction & {
status: 'running' | 'complete' | 'pending' | 'aborted';
abort?: () => void;
};
export type FailedState = BoltAction & {
status: 'failed';
error: string;
abort?: () => void;
};
export type ActionState = RunningState | FailedState;
export type ActionStateUpdate =
| { status: 'running' | 'complete' | 'pending' | 'aborted'; abort?: () => void }
| { status: 'failed'; error: string; abort?: () => void }
| { abort?: () => void };
export interface ArtifactState {
title: string;
closed: boolean;
currentActionPromise: Promise<void>;
actions: MapStore<Record<string, ActionState>>;
}
type Artifacts = MapStore<Record<string, ArtifactState>>;
export class WorkbenchStore {
#actionRunner = new ActionRunner(webcontainer);
#previewsStore = new PreviewsStore(webcontainer);
artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
get previews() {
return this.#previewsStore.previews;
}
setShowWorkbench(show: boolean) {
this.showWorkbench.set(show);
}
addArtifact({ id, messageId, title }: ArtifactCallbackData) {
const artifacts = this.artifacts.get();
const artifactKey = getArtifactKey(id, messageId);
const artifact = artifacts[artifactKey];
if (artifact) {
return;
}
this.artifacts.setKey(artifactKey, {
title,
closed: false,
actions: map({}),
currentActionPromise: Promise.resolve(),
});
}
updateArtifact({ id, messageId }: ArtifactCallbackData, state: Partial<ArtifactState>) {
const artifacts = this.artifacts.get();
const key = getArtifactKey(id, messageId);
const artifact = artifacts[key];
if (!artifact) {
return;
}
this.artifacts.setKey(key, { ...artifact, ...state });
}
async runAction(data: ActionCallbackData) {
const { artifactId, messageId, actionId } = data;
const artifacts = this.artifacts.get();
const key = getArtifactKey(artifactId, messageId);
const artifact = artifacts[key];
if (!artifact) {
unreachable('Artifact not found');
}
const actions = artifact.actions.get();
const action = actions[actionId];
if (action) {
return;
}
artifact.actions.setKey(actionId, { ...data.action, status: 'pending' });
artifact.currentActionPromise = artifact.currentActionPromise.then(async () => {
try {
let abortController: AbortController | undefined;
if (data.action.type === 'shell') {
abortController = new AbortController();
}
let aborted = false;
this.#updateAction(key, actionId, {
status: 'running',
abort: () => {
aborted = true;
abortController?.abort();
},
});
await this.#actionRunner.runAction(data, abortController?.signal);
this.#updateAction(key, actionId, { status: aborted ? 'aborted' : 'complete' });
} catch (error) {
this.#updateAction(key, actionId, { status: 'failed', error: 'Action failed' });
throw error;
}
});
}
#updateAction(artifactId: string, actionId: string, newState: ActionStateUpdate) {
const artifacts = this.artifacts.get();
const artifact = artifacts[artifactId];
if (!artifact) {
return;
}
const actions = artifact.actions.get();
artifact.actions.setKey(actionId, { ...actions[actionId], ...newState });
}
}
export function getArtifactKey(artifactId: string, messageId: string) {
return `${artifactId}_${messageId}`;
}
export const workbenchStore = new WorkbenchStore();
if (import.meta.hot) {
import.meta.hot.data.artifacts = workbenchStore.artifacts;
import.meta.hot.data.showWorkbench = workbenchStore.showWorkbench;
}

View File

@@ -1,42 +0,0 @@
import type { WebContainer } from '@webcontainer/api';
import { atom, map, type MapStore, type WritableAtom } from 'nanostores';
import { webcontainer } from '~/lib/webcontainer';
interface WorkspaceStoreOptions {
webcontainer: Promise<WebContainer>;
}
interface ArtifactState {
title: string;
closed: boolean;
actions: any /* TODO */;
}
export class WorkspaceStore {
#webcontainer: Promise<WebContainer>;
artifacts: MapStore<Record<string, ArtifactState>> = import.meta.hot?.data.artifacts ?? map({});
showWorkspace: WritableAtom<boolean> = import.meta.hot?.data.showWorkspace ?? atom(false);
constructor({ webcontainer }: WorkspaceStoreOptions) {
this.#webcontainer = webcontainer;
}
updateArtifact(id: string, state: Partial<ArtifactState>) {
const artifacts = this.artifacts.get();
const artifact = artifacts[id];
this.artifacts.setKey(id, { ...artifact, ...state });
}
runAction() {
// TODO
}
}
export const workspaceStore = new WorkspaceStore({ webcontainer });
if (import.meta.hot) {
import.meta.hot.data.artifacts = workspaceStore.artifacts;
import.meta.hot.data.showWorkspace = workspaceStore.showWorkspace;
}

View File

@@ -21,8 +21,9 @@ if (!import.meta.env.SSR) {
import.meta.hot?.data.webcontainer ??
Promise.resolve()
.then(() => WebContainer.boot({ workdirName: 'project' }))
.then(() => {
.then((webcontainer) => {
webcontainerContext.loaded = true;
return webcontainer;
});
if (import.meta.hot) {