codacus commited on
Commit
54351cd
·
1 Parent(s): 1ba0606

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

Browse files
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
@@ -72,7 +72,7 @@ export class ActionRunner {
72
  });
73
  }
74
 
75
- async runAction(data: ActionCallbackData) {
76
  const { actionId } = data;
77
  const action = this.actions.get()[actionId];
78
 
@@ -83,19 +83,22 @@ export class ActionRunner {
83
  if (action.executed) {
84
  return;
85
  }
 
 
 
86
 
87
- this.#updateAction(actionId, { ...action, ...data.action, executed: true });
88
 
89
  this.#currentExecutionPromise = this.#currentExecutionPromise
90
  .then(() => {
91
- return this.#executeAction(actionId);
92
  })
93
  .catch((error) => {
94
  console.error('Action failed:', error);
95
  });
96
  }
97
 
98
- async #executeAction(actionId: string) {
99
  const action = this.actions.get()[actionId];
100
 
101
  this.#updateAction(actionId, { status: 'running' });
@@ -112,7 +115,7 @@ export class ActionRunner {
112
  }
113
  }
114
 
115
- this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
116
  } catch (error) {
117
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
118
 
 
72
  });
73
  }
74
 
75
+ async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
76
  const { actionId } = data;
77
  const action = this.actions.get()[actionId];
78
 
 
83
  if (action.executed) {
84
  return;
85
  }
86
+ if (isStreaming && action.type !== 'file') {
87
+ return;
88
+ }
89
 
90
+ this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
91
 
92
  this.#currentExecutionPromise = this.#currentExecutionPromise
93
  .then(() => {
94
+ return this.#executeAction(actionId, isStreaming);
95
  })
96
  .catch((error) => {
97
  console.error('Action failed:', error);
98
  });
99
  }
100
 
101
+ async #executeAction(actionId: string, isStreaming: boolean = false) {
102
  const action = this.actions.get()[actionId];
103
 
104
  this.#updateAction(actionId, { status: 'running' });
 
115
  }
116
  }
117
 
118
+ this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
119
  } catch (error) {
120
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
121
 
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
 
@@ -54,7 +55,7 @@ interface MessageState {
54
  export class StreamingMessageParser {
55
  #messages = new Map<string, MessageState>();
56
 
57
- constructor(private _options: StreamingMessageParserOptions = {}) {}
58
 
59
  parse(messageId: string, input: string) {
60
  let state = this.#messages.get(messageId);
@@ -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
 
 
55
  export class StreamingMessageParser {
56
  #messages = new Map<string, MessageState>();
57
 
58
+ constructor(private _options: StreamingMessageParserOptions = {}) { }
59
 
60
  parse(messageId: string, input: string) {
61
  let state = this.#messages.get(messageId);
 
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
@@ -12,6 +12,7 @@ import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
  import { Octokit } from "@octokit/rest";
 
15
 
16
  export interface ArtifactState {
17
  id: string;
@@ -258,7 +259,7 @@ export class WorkbenchStore {
258
  artifact.runner.addAction(data);
259
  }
260
 
261
- async runAction(data: ActionCallbackData) {
262
  const { messageId } = data;
263
 
264
  const artifact = this.#getArtifact(messageId);
@@ -266,8 +267,29 @@ export class WorkbenchStore {
266
  if (!artifact) {
267
  unreachable('Artifact not found');
268
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
- artifact.runner.runAction(data);
 
 
 
 
 
 
 
 
271
  }
272
 
273
  #getArtifact(id: string) {
@@ -336,20 +358,20 @@ export class WorkbenchStore {
336
  }
337
 
338
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
339
-
340
  try {
341
  // Get the GitHub auth token from environment variables
342
  const githubToken = ghToken;
343
-
344
  const owner = githubUsername;
345
-
346
  if (!githubToken) {
347
  throw new Error('GitHub token is not set in environment variables');
348
  }
349
-
350
  // Initialize Octokit with the auth token
351
  const octokit = new Octokit({ auth: githubToken });
352
-
353
  // Check if the repository already exists before creating it
354
  let repo
355
  try {
@@ -368,13 +390,13 @@ export class WorkbenchStore {
368
  throw error; // Some other error occurred
369
  }
370
  }
371
-
372
  // Get all files
373
  const files = this.files.get();
374
  if (!files || Object.keys(files).length === 0) {
375
  throw new Error('No files found to push');
376
  }
377
-
378
  // Create blobs for each file
379
  const blobs = await Promise.all(
380
  Object.entries(files).map(async ([filePath, dirent]) => {
@@ -389,13 +411,13 @@ export class WorkbenchStore {
389
  }
390
  })
391
  );
392
-
393
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
394
-
395
  if (validBlobs.length === 0) {
396
  throw new Error('No valid files to push');
397
  }
398
-
399
  // Get the latest commit SHA (assuming main branch, update dynamically if needed)
400
  const { data: ref } = await octokit.git.getRef({
401
  owner: repo.owner.login,
@@ -403,7 +425,7 @@ export class WorkbenchStore {
403
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
404
  });
405
  const latestCommitSha = ref.object.sha;
406
-
407
  // Create a new tree
408
  const { data: newTree } = await octokit.git.createTree({
409
  owner: repo.owner.login,
@@ -416,7 +438,7 @@ export class WorkbenchStore {
416
  sha: blob!.sha,
417
  })),
418
  });
419
-
420
  // Create a new commit
421
  const { data: newCommit } = await octokit.git.createCommit({
422
  owner: repo.owner.login,
@@ -425,7 +447,7 @@ export class WorkbenchStore {
425
  tree: newTree.sha,
426
  parents: [latestCommitSha],
427
  });
428
-
429
  // Update the reference
430
  await octokit.git.updateRef({
431
  owner: repo.owner.login,
@@ -433,7 +455,7 @@ export class WorkbenchStore {
433
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
434
  sha: newCommit.sha,
435
  });
436
-
437
  alert(`Repository created and code pushed: ${repo.html_url}`);
438
  } catch (error) {
439
  console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
 
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
  import { Octokit } from "@octokit/rest";
15
+ import * as nodePath from 'node:path';
16
 
17
  export interface ArtifactState {
18
  id: string;
 
259
  artifact.runner.addAction(data);
260
  }
261
 
262
+ async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
263
  const { messageId } = data;
264
 
265
  const artifact = this.#getArtifact(messageId);
 
267
  if (!artifact) {
268
  unreachable('Artifact not found');
269
  }
270
+ if (data.action.type === 'file') {
271
+ let wc = await webcontainer
272
+ const fullPath = nodePath.join(wc.workdir, data.action.filePath);
273
+ if (this.selectedFile.value !== fullPath) {
274
+ this.setSelectedFile(fullPath);
275
+ }
276
+ if (this.currentView.value !== 'code') {
277
+ this.currentView.set('code');
278
+ }
279
+ const doc = this.#editorStore.documents.get()[fullPath];
280
+ if (!doc) {
281
+ await artifact.runner.runAction(data, isStreaming);
282
+ }
283
 
284
+ this.#editorStore.updateFile(fullPath, data.action.content);
285
+
286
+ if (!isStreaming) {
287
+ this.resetCurrentDocument();
288
+ await artifact.runner.runAction(data);
289
+ }
290
+ } else {
291
+ artifact.runner.runAction(data);
292
+ }
293
  }
294
 
295
  #getArtifact(id: string) {
 
358
  }
359
 
360
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
361
+
362
  try {
363
  // Get the GitHub auth token from environment variables
364
  const githubToken = ghToken;
365
+
366
  const owner = githubUsername;
367
+
368
  if (!githubToken) {
369
  throw new Error('GitHub token is not set in environment variables');
370
  }
371
+
372
  // Initialize Octokit with the auth token
373
  const octokit = new Octokit({ auth: githubToken });
374
+
375
  // Check if the repository already exists before creating it
376
  let repo
377
  try {
 
390
  throw error; // Some other error occurred
391
  }
392
  }
393
+
394
  // Get all files
395
  const files = this.files.get();
396
  if (!files || Object.keys(files).length === 0) {
397
  throw new Error('No files found to push');
398
  }
399
+
400
  // Create blobs for each file
401
  const blobs = await Promise.all(
402
  Object.entries(files).map(async ([filePath, dirent]) => {
 
411
  }
412
  })
413
  );
414
+
415
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
416
+
417
  if (validBlobs.length === 0) {
418
  throw new Error('No valid files to push');
419
  }
420
+
421
  // Get the latest commit SHA (assuming main branch, update dynamically if needed)
422
  const { data: ref } = await octokit.git.getRef({
423
  owner: repo.owner.login,
 
425
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
426
  });
427
  const latestCommitSha = ref.object.sha;
428
+
429
  // Create a new tree
430
  const { data: newTree } = await octokit.git.createTree({
431
  owner: repo.owner.login,
 
438
  sha: blob!.sha,
439
  })),
440
  });
441
+
442
  // Create a new commit
443
  const { data: newCommit } = await octokit.git.createCommit({
444
  owner: repo.owner.login,
 
447
  tree: newTree.sha,
448
  parents: [latestCommitSha],
449
  });
450
+
451
  // Update the reference
452
  await octokit.git.updateRef({
453
  owner: repo.owner.login,
 
455
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
456
  sha: newCommit.sha,
457
  });
458
+
459
  alert(`Repository created and code pushed: ${repo.html_url}`);
460
  } catch (error) {
461
  console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff