Chris Mahoney commited on
Commit
a081f8b
·
unverified ·
2 Parent(s): f1c5fbf 8c6e420

Merge pull request #213 from thecodacus/code-streaming

Browse files

feat(code-streaming): added code streaming to editor while AI is writing files

app/components/chat/BaseChat.tsx CHANGED
@@ -24,16 +24,16 @@ const EXAMPLE_PROMPTS = [
24
  { text: 'How do I center a div?' },
25
  ];
26
 
27
- const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
28
 
29
  const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
30
  return (
31
  <div className="mb-2 flex gap-2">
32
- <select
33
  value={provider}
34
  onChange={(e) => {
35
  setProvider(e.target.value);
36
- const firstModel = [...modelList].find(m => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
@@ -58,11 +58,13 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
58
  onChange={(e) => setModel(e.target.value)}
59
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
60
  >
61
- {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
62
- <option key={modelOption.name} value={modelOption.name}>
63
- {modelOption.label}
64
- </option>
65
- ))}
 
 
66
  </select>
67
  </div>
68
  );
@@ -81,10 +83,10 @@ interface BaseChatProps {
81
  enhancingPrompt?: boolean;
82
  promptEnhanced?: boolean;
83
  input?: string;
84
- model: string;
85
- setModel: (model: string) => void;
86
- provider: string;
87
- setProvider: (provider: string) => void;
88
  handleStop?: () => void;
89
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
90
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
@@ -144,7 +146,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
144
  expires: 30, // 30 days
145
  secure: true, // Only send over HTTPS
146
  sameSite: 'strict', // Protect against CSRF
147
- path: '/' // Accessible across the site
148
  });
149
  } catch (error) {
150
  console.error('Error saving API keys to cookies:', error);
@@ -281,7 +283,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
281
  </div>
282
  {input.length > 3 ? (
283
  <div className="text-xs text-bolt-elements-textTertiary">
284
- Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for a new line
 
 
285
  </div>
286
  ) : null}
287
  </div>
@@ -315,4 +319,4 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
315
  </div>
316
  );
317
  },
318
- );
 
24
  { text: 'How do I center a div?' },
25
  ];
26
 
27
+ const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))];
28
 
29
  const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
30
  return (
31
  <div className="mb-2 flex gap-2">
32
+ <select
33
  value={provider}
34
  onChange={(e) => {
35
  setProvider(e.target.value);
36
+ const firstModel = [...modelList].find((m) => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
 
58
  onChange={(e) => setModel(e.target.value)}
59
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
60
  >
61
+ {[...modelList]
62
+ .filter((e) => e.provider == provider && e.name)
63
+ .map((modelOption) => (
64
+ <option key={modelOption.name} value={modelOption.name}>
65
+ {modelOption.label}
66
+ </option>
67
+ ))}
68
  </select>
69
  </div>
70
  );
 
83
  enhancingPrompt?: boolean;
84
  promptEnhanced?: boolean;
85
  input?: string;
86
+ model?: string;
87
+ setModel?: (model: string) => void;
88
+ provider?: string;
89
+ setProvider?: (provider: string) => void;
90
  handleStop?: () => void;
91
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
92
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
 
146
  expires: 30, // 30 days
147
  secure: true, // Only send over HTTPS
148
  sameSite: 'strict', // Protect against CSRF
149
+ path: '/', // Accessible across the site
150
  });
151
  } catch (error) {
152
  console.error('Error saving API keys to cookies:', error);
 
283
  </div>
284
  {input.length > 3 ? (
285
  <div className="text-xs text-bolt-elements-textTertiary">
286
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
287
+ <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for
288
+ a new line
289
  </div>
290
  ) : null}
291
  </div>
 
319
  </div>
320
  );
321
  },
322
+ );
app/lib/hooks/useMessageParser.ts CHANGED
@@ -36,6 +36,10 @@ const messageParser = new StreamingMessageParser({
36
 
37
  workbenchStore.runAction(data);
38
  },
 
 
 
 
39
  },
40
  });
41
 
 
36
 
37
  workbenchStore.runAction(data);
38
  },
39
+ onActionStream: (data) => {
40
+ logger.trace('onActionStream', data.action);
41
+ workbenchStore.runAction(data, true);
42
+ },
43
  },
44
  });
45
 
app/lib/runtime/action-runner.ts CHANGED
@@ -77,7 +77,7 @@ export class ActionRunner {
77
  });
78
  }
79
 
80
- async runAction(data: ActionCallbackData) {
81
  const { actionId } = data;
82
  const action = this.actions.get()[actionId];
83
 
@@ -88,19 +88,22 @@ export class ActionRunner {
88
  if (action.executed) {
89
  return;
90
  }
 
 
 
91
 
92
- this.#updateAction(actionId, { ...action, ...data.action, executed: true });
93
 
94
  this.#currentExecutionPromise = this.#currentExecutionPromise
95
  .then(() => {
96
- return this.#executeAction(actionId);
97
  })
98
  .catch((error) => {
99
  console.error('Action failed:', error);
100
  });
101
  }
102
 
103
- async #executeAction(actionId: string) {
104
  const action = this.actions.get()[actionId];
105
 
106
  this.#updateAction(actionId, { status: 'running' });
@@ -121,7 +124,7 @@ export class ActionRunner {
121
  }
122
  }
123
 
124
- this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
125
  } catch (error) {
126
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
127
  logger.error(`[${action.type}]:Action failed\n\n`, error);
 
77
  });
78
  }
79
 
80
+ async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
81
  const { actionId } = data;
82
  const action = this.actions.get()[actionId];
83
 
 
88
  if (action.executed) {
89
  return;
90
  }
91
+ if (isStreaming && action.type !== 'file') {
92
+ return;
93
+ }
94
 
95
+ this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
96
 
97
  this.#currentExecutionPromise = this.#currentExecutionPromise
98
  .then(() => {
99
+ return this.#executeAction(actionId, isStreaming);
100
  })
101
  .catch((error) => {
102
  console.error('Action failed:', error);
103
  });
104
  }
105
 
106
+ async #executeAction(actionId: string, isStreaming: boolean = false) {
107
  const action = this.actions.get()[actionId];
108
 
109
  this.#updateAction(actionId, { status: 'running' });
 
124
  }
125
  }
126
 
127
+ this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
128
  } catch (error) {
129
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
130
  logger.error(`[${action.type}]:Action failed\n\n`, error);
app/lib/runtime/message-parser.ts CHANGED
@@ -28,6 +28,7 @@ export interface ParserCallbacks {
28
  onArtifactOpen?: ArtifactCallback;
29
  onArtifactClose?: ArtifactCallback;
30
  onActionOpen?: ActionCallback;
 
31
  onActionClose?: ActionCallback;
32
  }
33
 
@@ -118,6 +119,21 @@ export class StreamingMessageParser {
118
 
119
  i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
120
  } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  break;
122
  }
123
  } else {
 
28
  onArtifactOpen?: ArtifactCallback;
29
  onArtifactClose?: ArtifactCallback;
30
  onActionOpen?: ActionCallback;
31
+ onActionStream?: ActionCallback;
32
  onActionClose?: ActionCallback;
33
  }
34
 
 
119
 
120
  i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
121
  } else {
122
+ if ('type' in currentAction && currentAction.type === 'file') {
123
+ let content = input.slice(i);
124
+
125
+ this._options.callbacks?.onActionStream?.({
126
+ artifactId: currentArtifact.id,
127
+ messageId,
128
+ actionId: String(state.actionId - 1),
129
+ action: {
130
+ ...currentAction as FileAction,
131
+ content,
132
+ filePath: currentAction.filePath,
133
+ },
134
+
135
+ });
136
+ }
137
  break;
138
  }
139
  } else {
app/lib/stores/workbench.ts CHANGED
@@ -11,7 +11,8 @@ import { PreviewsStore } from './previews';
11
  import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
- import { Octokit } from "@octokit/rest";
 
15
  import type { WebContainerProcess } from '@webcontainer/api';
16
 
17
  export interface ArtifactState {
@@ -267,7 +268,7 @@ export class WorkbenchStore {
267
  artifact.runner.addAction(data);
268
  }
269
 
270
- async runAction(data: ActionCallbackData) {
271
  const { messageId } = data;
272
 
273
  const artifact = this.#getArtifact(messageId);
@@ -275,8 +276,29 @@ export class WorkbenchStore {
275
  if (!artifact) {
276
  unreachable('Artifact not found');
277
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
- artifact.runner.runAction(data);
 
 
 
 
 
 
 
 
280
  }
281
 
282
  #getArtifact(id: string) {
@@ -360,9 +382,10 @@ export class WorkbenchStore {
360
  const octokit = new Octokit({ auth: githubToken });
361
 
362
  // Check if the repository already exists before creating it
363
- let repo
364
  try {
365
- repo = await octokit.repos.get({ owner: owner, repo: repoName });
 
366
  } catch (error) {
367
  if (error instanceof Error && 'status' in error && error.status === 404) {
368
  // Repository doesn't exist, so create a new one
 
11
  import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
+ import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
15
+ import * as nodePath from 'node:path';
16
  import type { WebContainerProcess } from '@webcontainer/api';
17
 
18
  export interface ArtifactState {
 
268
  artifact.runner.addAction(data);
269
  }
270
 
271
+ async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
272
  const { messageId } = data;
273
 
274
  const artifact = this.#getArtifact(messageId);
 
276
  if (!artifact) {
277
  unreachable('Artifact not found');
278
  }
279
+ if (data.action.type === 'file') {
280
+ let wc = await webcontainer
281
+ const fullPath = nodePath.join(wc.workdir, data.action.filePath);
282
+ if (this.selectedFile.value !== fullPath) {
283
+ this.setSelectedFile(fullPath);
284
+ }
285
+ if (this.currentView.value !== 'code') {
286
+ this.currentView.set('code');
287
+ }
288
+ const doc = this.#editorStore.documents.get()[fullPath];
289
+ if (!doc) {
290
+ await artifact.runner.runAction(data, isStreaming);
291
+ }
292
 
293
+ this.#editorStore.updateFile(fullPath, data.action.content);
294
+
295
+ if (!isStreaming) {
296
+ this.resetCurrentDocument();
297
+ await artifact.runner.runAction(data);
298
+ }
299
+ } else {
300
+ artifact.runner.runAction(data);
301
+ }
302
  }
303
 
304
  #getArtifact(id: string) {
 
382
  const octokit = new Octokit({ auth: githubToken });
383
 
384
  // Check if the repository already exists before creating it
385
+ let repo: RestEndpointMethodTypes["repos"]["get"]["response"]['data']
386
  try {
387
+ let resp = await octokit.repos.get({ owner: owner, repo: repoName });
388
+ repo = resp.data
389
  } catch (error) {
390
  if (error instanceof Error && 'status' in error && error.status === 404) {
391
  // Repository doesn't exist, so create a new one
package.json CHANGED
@@ -117,5 +117,5 @@
117
  "resolutions": {
118
  "@typescript-eslint/utils": "^8.0.0-alpha.30"
119
  },
120
- "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
121
  }
 
117
  "resolutions": {
118
  "@typescript-eslint/utils": "^8.0.0-alpha.30"
119
  },
120
+ "packageManager": "pnpm@9.4.0"
121
  }