vzcode 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-836bdce7.css +1 -0
  3. package/dist/assets/index-c7f894a9.js +115 -0
  4. package/dist/assets/worker-a2979249.js +366 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +11 -5
  7. package/src/client/AIAssist.ts +137 -0
  8. package/src/client/AIAssistWidget/index.tsx +101 -0
  9. package/src/client/AIAssistWidget/style.scss +10 -0
  10. package/src/client/App/VZCodeContext.tsx +251 -0
  11. package/src/client/App/VZLeft.tsx +56 -0
  12. package/src/client/App/VZMiddle.tsx +91 -0
  13. package/src/client/App/VZRight.tsx +15 -0
  14. package/src/client/App/index.tsx +104 -0
  15. package/src/client/{style.scss → App/style.scss} +14 -3
  16. package/src/client/App/useShareDB.ts +103 -0
  17. package/src/client/CodeEditor/getOrCreateEditor.ts +70 -7
  18. package/src/client/CodeEditor/index.tsx +24 -0
  19. package/src/client/CodeEditor/json1PresenceDisplay.ts +38 -9
  20. package/src/client/CodeEditor/typeScriptCompletions.ts +108 -0
  21. package/src/client/CodeEditor/typeScriptLinter.ts +45 -0
  22. package/src/client/CodeErrorOverlay/index.tsx +44 -8
  23. package/src/client/CodeErrorOverlay/style.scss +22 -11
  24. package/src/client/SplitPaneResizeContext.tsx +195 -39
  25. package/src/client/TabList/index.tsx +1 -27
  26. package/src/client/{Resizer → VZResizer}/index.tsx +37 -13
  27. package/src/client/VZSettings.tsx +1 -1
  28. package/src/client/VZSidebar/CreateFileModal.tsx +93 -0
  29. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +42 -24
  30. package/src/client/VZSidebar/index.tsx +116 -33
  31. package/src/client/VZSidebar/styles.scss +21 -1
  32. package/src/client/useFileCRUD.ts +19 -19
  33. package/src/client/useOpenDirectories.ts +12 -8
  34. package/src/client/usePrettier/index.ts +13 -1
  35. package/src/client/useTypeScript/index.ts +44 -0
  36. package/src/client/useTypeScript/requestTypes.ts +29 -0
  37. package/src/client/useTypeScript/worker.ts +222 -0
  38. package/src/client/usernameLocalStorage.ts +1 -1
  39. package/src/client/vzReducer/createInitialState.ts +4 -5
  40. package/src/index.ts +5 -1
  41. package/src/server/handleAIAssist.js +133 -0
  42. package/src/server/index.js +63 -1
  43. package/src/types.ts +78 -1
  44. package/dist/assets/index-5f9f376e.css +0 -1
  45. package/dist/assets/index-70b6d947.js +0 -111
  46. package/src/client/App.tsx +0 -286
  47. /package/src/client/{Resizer → VZResizer}/styles.scss +0 -0
@@ -0,0 +1,222 @@
1
+ import * as tsvfs from '@typescript/vfs';
2
+ import ts from 'typescript';
3
+ import { File, Files, VZCodeContent } from '../../types';
4
+ import {
5
+ AutocompleteRequest,
6
+ AutocompleteResponse,
7
+ LinterRequest,
8
+ LinterResponse,
9
+ } from './requestTypes';
10
+
11
+ let isFileSystemInitialized = false;
12
+
13
+ let env: tsvfs.VirtualTypeScriptEnvironment = null;
14
+
15
+ const debug = false;
16
+
17
+ // replace .js or .jsx with .ts or .tsx,
18
+ // to support TypeScript completions on non-TS files.
19
+ const getTSFileName = (fileName: string) => {
20
+ if (fileName.endsWith('.js')) {
21
+ return fileName.replace('.js', '.ts');
22
+ }
23
+ if (fileName.endsWith('.jsx')) {
24
+ return fileName.replace('.jsx', '.tsx');
25
+ }
26
+ return fileName;
27
+ };
28
+
29
+ // Returns true if the file name ends with `.ts` or `.tsx`.
30
+ const isTS = (fileName: string) => {
31
+ return (
32
+ fileName.endsWith('.ts') || fileName.endsWith('.tsx')
33
+ );
34
+ };
35
+
36
+ // This is a place for things we only do _once_.
37
+ const initializeFileSystem = async () => {
38
+ if (debug) {
39
+ console.log('initializeFileSystem');
40
+ }
41
+ const compilerOptions: ts.CompilerOptions = {};
42
+
43
+ // `true` breaks in a Web Worker because
44
+ // this uses `localStorage` under the hood,
45
+ // which is not available in a Web Worker.
46
+ const cache = false;
47
+
48
+ const fsMap = await tsvfs.createDefaultMapFromCDN(
49
+ compilerOptions,
50
+ ts.version,
51
+ cache,
52
+ ts,
53
+ );
54
+ const sys: ts.System = tsvfs.createSystem(fsMap);
55
+
56
+ // We'll add files to this later.
57
+ const rootFiles = [];
58
+
59
+ env = tsvfs.createVirtualTypeScriptEnvironment(
60
+ sys,
61
+ rootFiles,
62
+ ts,
63
+ compilerOptions,
64
+ );
65
+
66
+ if (debug) {
67
+ console.log('initializeFileSystem done');
68
+ }
69
+ };
70
+
71
+ const setFile = (tsFileName: string, text: string) => {
72
+ const existingFile = env.getSourceFile(tsFileName);
73
+ if (existingFile === undefined) {
74
+ env.createFile(tsFileName, text);
75
+ } else {
76
+ env.updateFile(tsFileName, text);
77
+ }
78
+ };
79
+
80
+ // Inspired by: https://stackblitz.com/edit/codemirror-6-typescript?file=client%2Findex.ts%3AL44-L44
81
+ const convertToCodeMirrorDiagnostic = (
82
+ tsErrors: ts.Diagnostic[],
83
+ ) => {
84
+ return tsErrors.map((tsError: ts.Diagnostic) => ({
85
+ from: tsError.start,
86
+ to: tsError.start + tsError.length,
87
+ severity: 'error',
88
+ message:
89
+ typeof tsError.messageText === 'string'
90
+ ? tsError.messageText
91
+ : tsError.messageText.messageText,
92
+ }));
93
+ };
94
+
95
+ onmessage = async ({ data }) => {
96
+ if (debug) {
97
+ console.log('message received');
98
+ }
99
+ // Initialize the file system.
100
+ if (!isFileSystemInitialized) {
101
+ isFileSystemInitialized = true;
102
+ await initializeFileSystem();
103
+ }
104
+
105
+ // Sanity check.
106
+ if (env === null) {
107
+ throw new Error('File system not initialized');
108
+ }
109
+
110
+ // Handle the update-content event, which
111
+ // updates the files as they change.
112
+ // This handles the cases where:
113
+ // * The file system is populated for the first time.
114
+ // * Files are edited by remote users.
115
+ if (data.event === 'update-content') {
116
+ if (debug) {
117
+ console.log('update-content message received');
118
+ }
119
+ // Unpack the files
120
+ const content: VZCodeContent = data.details;
121
+ const files: Files = content.files;
122
+
123
+ // Iterate over the files
124
+
125
+ for (const fileId of Object.keys(files)) {
126
+ const file: File = files[fileId];
127
+ const { name, text } = file;
128
+
129
+ const tsFileName = getTSFileName(name);
130
+
131
+ if (!isTS(tsFileName)) {
132
+ continue;
133
+ }
134
+
135
+ setFile(tsFileName, text);
136
+ // TODO - Handle renaming files.
137
+ // TODO - Handle deleting files.
138
+ // TODO - Handle directories.
139
+ }
140
+ }
141
+
142
+ if (data.event === 'autocomplete-request') {
143
+ if (debug) {
144
+ console.log('autocomplete-request message received');
145
+ }
146
+ // Should not happen.
147
+ if (env === null) {
148
+ console.log('env is null');
149
+ return;
150
+ }
151
+
152
+ // Example of `data`:
153
+ // {
154
+ // "event": "autocomplete-request",
155
+ // "pos": 8,
156
+ // "location": "index.js",
157
+ // "requestId": "0.9090605799171392"
158
+ // }
159
+
160
+ const autocompleteRequest: AutocompleteRequest = data;
161
+ const { fileName, fileContent, position, requestId } =
162
+ autocompleteRequest;
163
+
164
+ const tsFileName = getTSFileName(fileName);
165
+
166
+ let completions = null;
167
+ if (isTS(tsFileName)) {
168
+ // Update the file in the file system to the
169
+ // absolute latest version. This is critical
170
+ // for correct completions.
171
+ setFile(tsFileName, fileContent);
172
+
173
+ completions =
174
+ env.languageService.getCompletionsAtPosition(
175
+ tsFileName,
176
+ position,
177
+ {},
178
+ );
179
+ }
180
+
181
+ const autocompleteResponse: AutocompleteResponse = {
182
+ event: 'post-completions',
183
+ completions,
184
+ requestId,
185
+ };
186
+
187
+ postMessage(autocompleteResponse);
188
+ }
189
+
190
+ if (data.event == 'lint-request') {
191
+ if (debug) {
192
+ console.log('Lint Request');
193
+ }
194
+ const linterRequest: LinterRequest = data;
195
+ const { fileName, fileContent, requestId } =
196
+ linterRequest;
197
+
198
+ const tsFileName = getTSFileName(fileName);
199
+ let tsErrors = null;
200
+ // Since we are also updating the server when we autocomplete we do not need to update
201
+ if (isTS(tsFileName)) {
202
+ setFile(tsFileName, fileContent);
203
+ // Creates an array of diagnostic objects containing
204
+ // both semantic and syntactic diagnostics.
205
+ tsErrors = env.languageService
206
+ .getSemanticDiagnostics(fileName)
207
+ .concat(
208
+ env.languageService.getSyntacticDiagnostics(
209
+ fileName,
210
+ ),
211
+ );
212
+ tsErrors = convertToCodeMirrorDiagnostic(tsErrors);
213
+ }
214
+
215
+ const linterResponse: LinterResponse = {
216
+ event: 'post-error-linter',
217
+ tsErrors,
218
+ requestId,
219
+ };
220
+ postMessage(linterResponse);
221
+ }
222
+ };
@@ -1,5 +1,5 @@
1
1
  // This module deals with storing and accessing the
2
-
2
+ // username in local storage.
3
3
  import { useEffect, useRef } from 'react';
4
4
  import { Username } from '../types';
5
5
 
@@ -1,19 +1,18 @@
1
1
  import { VZState } from '.';
2
+ import { Username } from '../../types';
2
3
  import { ThemeLabel } from '../themes';
3
4
 
4
5
  export const createInitialState = ({
5
6
  defaultTheme,
6
- initialUsername,
7
+ initialUsername = 'Anonymous',
7
8
  }: {
8
9
  defaultTheme: ThemeLabel;
9
- initialUsername?: string;
10
+ initialUsername?: Username;
10
11
  }): VZState => ({
11
12
  tabList: [],
12
13
  activeFileId: null,
13
14
  theme: defaultTheme,
14
15
  isSettingsOpen: false,
15
16
  editorWantsFocus: false,
16
-
17
- // TODO unify this with App.tsx
18
- username: initialUsername || 'Anonymous',
17
+ username: initialUsername,
19
18
  });
package/src/index.ts CHANGED
@@ -13,13 +13,17 @@ export type {
13
13
  ThemeOption,
14
14
  } from './client/themes';
15
15
 
16
+ export { VZCodeProvider } from './client/App/VZCodeContext';
16
17
  export { VZSidebar } from './client/VZSidebar';
17
18
  export { VZSettings } from './client/VZSettings';
19
+ export { VZResizer } from './client/VZResizer';
20
+ export { VZLeft } from './client/App/VZLeft';
21
+ export { VZMiddle } from './client/App/VZMiddle';
22
+
18
23
  export {
19
24
  SplitPaneResizeProvider,
20
25
  SplitPaneResizeContext,
21
26
  } from './client/SplitPaneResizeContext';
22
- export { Resizer } from './client/Resizer';
23
27
  export { TabList } from './client/TabList';
24
28
  export { CodeEditor } from './client/CodeEditor';
25
29
  export { usePrettier } from './client/usePrettier';
@@ -0,0 +1,133 @@
1
+ import OpenAI from 'openai';
2
+ import { editOp, type, replaceOp } from 'ot-json1';
3
+
4
+ // Feature flag to slow down AI for development/testing
5
+ const slowdown = false;
6
+
7
+ let openai;
8
+ if (process.env.OPENAI_API_KEY !== undefined) {
9
+ openai = new OpenAI();
10
+ }
11
+
12
+ export async function generateAIResponse({
13
+ inputText,
14
+ insertionCursor,
15
+ fileId,
16
+ streamId,
17
+ shareDBDoc,
18
+ }) {
19
+ function accomodateDocChanges(op, source) {
20
+ if (!opComesFromAIAssist(op, source)) {
21
+ if (op !== null) {
22
+ insertionCursor = type
23
+ .transformPosition(
24
+ ['files', fileId, 'text', insertionCursor],
25
+ op,
26
+ )
27
+ .slice(-1)[0];
28
+ }
29
+ }
30
+ }
31
+
32
+ shareDBDoc.on('op', accomodateDocChanges);
33
+
34
+ streams[streamId] = await openai.chat.completions.create({
35
+ // model: 'gpt-3.5-turbo',
36
+ model: 'gpt-4',
37
+
38
+ messages: [
39
+ {
40
+ role: 'system',
41
+ content:
42
+ 'Write typescript or javascript code to continue the current file, given the other files for context. Use // for comments.',
43
+ },
44
+ { role: 'user', content: inputText },
45
+ ],
46
+ stream: true,
47
+ });
48
+
49
+ for await (const part of streams[streamId]) {
50
+ const op = editOp(
51
+ ['files', fileId, 'text'],
52
+ 'text-unicode',
53
+ [
54
+ insertionCursor,
55
+ part.choices[0]?.delta?.content || '',
56
+ ],
57
+ );
58
+
59
+ shareDBDoc.submitOp(op, { source: AISourceName });
60
+
61
+ // Wait for 500ms
62
+ if (slowdown) {
63
+ await new Promise((resolve) => {
64
+ setTimeout(resolve, 1000);
65
+ });
66
+ }
67
+
68
+ insertionCursor += (
69
+ part.choices[0]?.delta?.content || ''
70
+ ).length;
71
+ }
72
+ shareDBDoc.off('op', accomodateDocChanges);
73
+
74
+ const confirmCompletedOperation = replaceOp(
75
+ [
76
+ 'aiStreams',
77
+ streamId,
78
+ 'AIStreamStatus',
79
+ 'serverIsRunning',
80
+ ],
81
+ true,
82
+ false,
83
+ );
84
+ shareDBDoc.submitOp(confirmCompletedOperation, {
85
+ source: 'AIServer',
86
+ });
87
+ }
88
+
89
+ const AISourceName = 'AIAssist';
90
+
91
+ function opComesFromAIAssist(ops, source) {
92
+ return source === AISourceName;
93
+ }
94
+
95
+ const streams = {};
96
+
97
+ export const handleAIAssist =
98
+ (shareDBDoc) => async (req, res) => {
99
+ const {
100
+ text: inputText,
101
+ cursorLocation: insertionCursor,
102
+ fileId,
103
+ } = req.body;
104
+
105
+ try {
106
+ await generateAIResponse({
107
+ inputText,
108
+ insertionCursor,
109
+ fileId,
110
+ shareDBDoc,
111
+ });
112
+
113
+ res
114
+ .status(200)
115
+ .send({ message: 'Operation successful!' });
116
+ } catch (error) {
117
+ console.error('handleAIAssist error:', error);
118
+ res.status(500).send({
119
+ message: 'Internal Server Error',
120
+ error: error.message,
121
+ });
122
+ }
123
+ };
124
+
125
+ export function haltGeneration(streamId) {
126
+ const stream = streams[streamId];
127
+
128
+ // Stream can be undefined here if the user
129
+ // clicks start and stop very quickly.
130
+ if (stream) {
131
+ stream.controller.abort();
132
+ }
133
+ }
@@ -12,6 +12,13 @@ import ngrok from 'ngrok';
12
12
  import dotenv from 'dotenv';
13
13
  import { computeInitialDocument } from './computeInitialDocument.js';
14
14
  import { json1Presence } from '../ot.js';
15
+ import bodyParser from 'body-parser';
16
+ import {
17
+ generateAIResponse,
18
+ haltGeneration,
19
+ handleAIAssist,
20
+ } from './handleAIAssist.js';
21
+ import { replaceOp } from 'ot-json1';
15
22
 
16
23
  dotenv.config({ path: '../../.env' });
17
24
 
@@ -95,6 +102,13 @@ const shareDBConnection = shareDBBackend.connect();
95
102
  const shareDBDoc = shareDBConnection.get('documents', '1');
96
103
  shareDBDoc.create(initialDocument, json1Presence.type.uri);
97
104
 
105
+ // Handle AI Assist requests.
106
+ app.post(
107
+ '/AIAssist',
108
+ bodyParser.json(),
109
+ handleAIAssist(shareDBDoc),
110
+ );
111
+
98
112
  // The state of the document when files were last auto-saved.
99
113
  let previousDocument = initialDocument;
100
114
 
@@ -199,12 +213,60 @@ function debounceSave() {
199
213
 
200
214
  // Subscribe to listen for modifications
201
215
  shareDBDoc.subscribe(() => {
202
- shareDBDoc.on('op', (op) => {
216
+ shareDBDoc.on('op', (op, source) => {
203
217
  if (shareDBDoc.data.isInteracting) {
204
218
  throttleSave();
205
219
  } else {
206
220
  debounceSave();
207
221
  }
222
+
223
+ if (
224
+ op !== null &&
225
+ op[0] == 'aiStreams' &&
226
+ source !== 'AIServer' &&
227
+ source !== 'AIAssist'
228
+ ) {
229
+ //This is an insert operation (a new request)
230
+ if (
231
+ op[op.length - 1]['i'] !== undefined &&
232
+ op[op.length - 1]['i']['AIStreamStatus'] !==
233
+ undefined
234
+ ) {
235
+ const input =
236
+ op[op.length - 1]['i']['AIStreamStatus'];
237
+
238
+ generateAIResponse({
239
+ inputText: input.text,
240
+ insertionCursor: input.insertionCursor,
241
+ shareDBDoc: shareDBDoc,
242
+ streamId: op[op.length - 2],
243
+ fileId: input.fileId,
244
+ });
245
+
246
+ const confirmStartOperation = replaceOp(
247
+ [
248
+ ...op.filter(
249
+ (value) => typeof value === 'string',
250
+ ),
251
+ 'AIStreamStatus',
252
+ 'serverIsRunning',
253
+ ],
254
+ true,
255
+ true,
256
+ );
257
+
258
+ shareDBDoc.submitOp(confirmStartOperation, {
259
+ source: 'AIServer',
260
+ });
261
+ }
262
+ if (
263
+ op[op.length - 1]['r'] !== null &&
264
+ op[op.length - 2] == 'clientWantsToStart'
265
+ ) {
266
+ //client wants to halt generation
267
+ haltGeneration(op[1]);
268
+ }
269
+ }
208
270
  });
209
271
  });
210
272
 
package/src/types.ts CHANGED
@@ -72,7 +72,10 @@ export type ShareDBDoc<T> = {
72
72
  event: string,
73
73
  callback: (op: JSONOp, source: boolean) => void,
74
74
  ) => void;
75
- off: (event: string, callback: () => void) => void;
75
+ off: (
76
+ event: string,
77
+ callback: (op: JSONOp, source: boolean) => void,
78
+ ) => void;
76
79
  submitOp: (
77
80
  op: JSONOp,
78
81
  options?: any,
@@ -80,6 +83,61 @@ export type ShareDBDoc<T> = {
80
83
  ) => void;
81
84
  };
82
85
 
86
+ // A unique ID for an AI stream.
87
+ // Generated by the client.
88
+ export type AIStreamId = string;
89
+
90
+ // TODO define this interface.
91
+ export type AIStreamStatus = {
92
+ // This field is managed by the client.
93
+ // * `true` if the client wants the server to start the generation.
94
+ // * `false` if the client does not want the server to start the generation.
95
+ clientWantsToStart: boolean;
96
+
97
+ // This field is managed by the server.
98
+ // * `true` if the server is running the generation.
99
+ // * `false` if the server is not running the generation.
100
+ serverIsRunning: boolean;
101
+
102
+ text: string;
103
+
104
+ startingInsertionCursor: number;
105
+ fileId: FileId;
106
+ };
107
+
108
+ /// Alternative universe
109
+
110
+ // // // If the generation started yet or not
111
+ // // // * If not, the server will start it.
112
+ // // started: boolean;
113
+
114
+ // // Required?
115
+ // // aiStreamId: AIStreamId
116
+
117
+ // // The generation status
118
+ // // * "running" if the generation is running
119
+ // // * "finished" if the generation is finished
120
+ // // * "error" if the generation errored
121
+ // // * "stopped" if the generation was stopped
122
+ // status:
123
+ // | 'unstarted'
124
+ // | 'running'
125
+ // | 'finished'
126
+ // | 'error'
127
+ // | 'stopped';
128
+
129
+ // // Possible states:
130
+ // // * { started: 'unstarted' } - client added this,
131
+ // // It should trigger the server to start the generation.
132
+ // // * { status: 'running' } - server added this,
133
+ // // It indicates that the generation is running.
134
+ // // * { status: 'finished' } - server added this,
135
+ // // It indicates that the generation is finished.
136
+ // // * { status: 'stopped' } - server added this,
137
+ // // It indicates that the generation was stopped.
138
+ // //
139
+ // // After `status` transitions to `finished` or `stopped`,
140
+
83
141
  // The ShareDB document type for VZCode.
84
142
  export type VZCodeContent = {
85
143
  // `files`
@@ -93,6 +151,25 @@ export type VZCodeContent = {
93
151
  // * `false` or `undefined` when they are not (e.g. normal typing)
94
152
  // * Hot reloading is debounced when this is `false`.
95
153
  isInteracting?: boolean;
154
+
155
+ // `aiStreams`
156
+ // * The AI streams in the VZCode instance.
157
+ // * Keys are AI stream IDs.
158
+ // * Values are AI streams.
159
+ // What is this for?
160
+ // * Synchronize the status of streams between the
161
+ // client and server.
162
+ // * This can power the transition of the icon from
163
+ // lightning bolt to stop sign and back (when stream finishes)
164
+ aiStreams: {
165
+ // These need to be removed at some point?
166
+ [aiStreamId: AIStreamId]: {
167
+ AIStreamStatus: AIStreamStatus;
168
+ };
169
+ };
170
+
171
+ // TODO consider is there a way to replace the HTTP request to
172
+ // the AIAssist endpoint with a manipulation of the ShareDB document?
96
173
  };
97
174
 
98
175
  // An id used for presence.