vzcode 0.44.0 → 0.46.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 (32) hide show
  1. package/dist/assets/{index-7MNYa9sP.css → index-0U2uzpmH.css} +1 -1
  2. package/dist/assets/{index-f4vqBOE4.js → index-iJQOJ5Kf.js} +38 -38
  3. package/dist/index.html +2 -2
  4. package/package.json +7 -7
  5. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
  6. package/src/client/AIAssist/AIAssistWidget/index.tsx +96 -0
  7. package/src/client/AIAssist/AIAssistWidget/style.scss +19 -0
  8. package/src/client/AIAssist/generateFilesContext.ts +25 -0
  9. package/src/client/AIAssist/index.ts +3 -0
  10. package/src/client/AIAssist/startAIAssist.ts +93 -0
  11. package/src/client/CodeEditor/getOrCreateEditor.ts +9 -18
  12. package/src/client/CodeEditor/index.tsx +0 -16
  13. package/src/client/CodeEditor/typeScriptCompletions.ts +1 -5
  14. package/src/client/CodeEditor/typeScriptLinter.ts +2 -2
  15. package/src/client/CodeErrorOverlay/index.tsx +23 -18
  16. package/src/client/Icons/SparklesSVG.tsx +31 -0
  17. package/src/client/Icons/StopSVG.tsx +15 -0
  18. package/src/client/Icons/index.tsx +2 -0
  19. package/src/client/VZMiddle.tsx +20 -8
  20. package/src/client/generateRequestId.ts +6 -0
  21. package/src/client/useTypeScript/index.ts +4 -0
  22. package/src/server/computeInitialDocument.js +26 -37
  23. package/src/server/config.js +24 -0
  24. package/src/server/featureFlags.js +7 -1
  25. package/src/server/generateAIResponse.js +101 -0
  26. package/src/server/handleAIAssist.js +11 -100
  27. package/src/server/index.js +50 -58
  28. package/src/server/setupEnv.js +18 -0
  29. package/src/types.ts +23 -23
  30. package/src/client/AIAssist.ts +0 -133
  31. package/src/client/AIAssistWidget/index.tsx +0 -101
  32. package/src/client/AIAssistWidget/style.scss +0 -10
@@ -4,8 +4,10 @@ import { randomId } from '../randomId.js';
4
4
  import {
5
5
  enableDirectories,
6
6
  debugDirectories,
7
+ debugIgnore,
7
8
  } from './featureFlags.js';
8
9
  import createIgnore from 'ignore';
10
+ import { ignoreFilePattern, baseIgnore } from './config.js';
9
11
 
10
12
  const isDirectory = (file) => file.endsWith('/');
11
13
 
@@ -27,7 +29,7 @@ const parseIgnoreFile = (
27
29
  );
28
30
  const content = fs.readFileSync(filePath, 'utf8');
29
31
  const globs = content
30
- .split(/(?:[\n\r])+/)
32
+ .split(/[\n\r]+/)
31
33
  .filter(
32
34
  // remove blank line and comments
33
35
  (line) => line.length > 0 && !line.startsWith('#'),
@@ -40,7 +42,7 @@ const parseIgnoreFile = (
40
42
  Boolean(slash) || /\/.*\S/.test(glob);
41
43
  const relativeGlob = path.posix.join(
42
44
  currentDirectory.replace(
43
- // escape characters ith special meaning in glob expressions
45
+ // escape characters with special meaning in glob expressions
44
46
  /[*?!# \[\]\\]/g,
45
47
  (char) => '\\' + char,
46
48
  ),
@@ -48,10 +50,10 @@ const parseIgnoreFile = (
48
50
  hasSlash ? '' : '**',
49
51
  glob,
50
52
  );
51
- // preserve leading ! and / characters
53
+ // preserve leading `!` and `/` characters
52
54
  return bang + slash + relativeGlob;
53
55
  });
54
- if (debugDirectories) {
56
+ if (debugIgnore) {
55
57
  console.debug(
56
58
  'at',
57
59
  currentDirectory,
@@ -94,18 +96,21 @@ export const computeInitialDocument = ({ fullPath }) => {
94
96
  */
95
97
  let files = [];
96
98
 
97
- // default paths to ignore
98
- const globs = ['.git'];
99
- const ignoreStack = [
100
- { ignore: createIgnore().add('globs'), globs },
101
- ];
99
+ const ignoreFileMatcher = createIgnore().add(
100
+ ignoreFilePattern,
101
+ );
102
+ const isIgnoreFile = (fileName) =>
103
+ ignoreFileMatcher.ignores(fileName);
102
104
 
103
105
  const unsearchedDirectories = [
104
- { currentDirectory: '.', ignoreStack },
106
+ {
107
+ currentDirectory: '.',
108
+ ignore: createIgnore().add(baseIgnore),
109
+ },
105
110
  ];
106
111
 
107
112
  while (unsearchedDirectories.length !== 0) {
108
- const { currentDirectory, ignoreStack } =
113
+ const { currentDirectory, ignore: parentIgnore } =
109
114
  unsearchedDirectories.pop();
110
115
  const currentDirectoryPath = path.join(
111
116
  fullPath,
@@ -122,13 +127,10 @@ export const computeInitialDocument = ({ fullPath }) => {
122
127
  const ignoreFiles = dirEntries
123
128
  .filter(
124
129
  (dirent) =>
125
- dirent.isFile() &&
126
- /^\.(?:git)?ignore$/.test(dirent.name),
130
+ dirent.isFile() && isIgnoreFile(dirent.name),
127
131
  )
128
132
  .map((file) => file.name);
129
- const ignoreEntry = ignoreStack.at(-1);
130
- let { ignore } = ignoreEntry;
131
- let newIgnoreStack = ignoreStack;
133
+ let ignore = parentIgnore;
132
134
  if (ignoreFiles.length > 0) {
133
135
  const globs = ignoreFiles.flatMap((fileName) =>
134
136
  parseIgnoreFile(
@@ -137,33 +139,20 @@ export const computeInitialDocument = ({ fullPath }) => {
137
139
  fileName,
138
140
  ),
139
141
  );
140
- const newEntry = { ignore, globs };
141
- newIgnoreStack = ignoreStack.slice();
142
- newIgnoreStack.push(newEntry);
143
- ignore = createIgnore().add(
144
- newIgnoreStack.flatMap((entry) => entry.globs),
145
- );
146
- newEntry.ignore = ignore;
147
- if (debugDirectories) {
148
- console.debug(
149
- 'at',
150
- currentDirectory,
151
- 'ignoring globs',
152
- newIgnoreStack.map((item) => item.globs),
153
- );
154
- }
142
+ ignore = createIgnore().add(parentIgnore).add(globs);
155
143
  }
156
144
  const newFiles = dirEntries
157
145
  .filter((dirent) => {
158
- let keep = !ignore.ignores(
159
- path.posix.join(currentDirectory, dirent.name),
160
- );
161
- if (debugDirectories && !keep) {
146
+ const relativePath =
147
+ path.posix.join(currentDirectory, dirent.name) +
148
+ (dirent.isDirectory() ? '/' : '');
149
+ const keep = !ignore.ignores(relativePath);
150
+ if (debugIgnore && !keep) {
162
151
  console.debug(
163
152
  'at',
164
153
  currentDirectory,
165
154
  'ignoring',
166
- dirent.name,
155
+ relativePath,
167
156
  );
168
157
  }
169
158
  return keep;
@@ -180,7 +169,7 @@ export const computeInitialDocument = ({ fullPath }) => {
180
169
  }
181
170
  unsearchedDirectories.push({
182
171
  currentDirectory: relativePath,
183
- ignoreStack: newIgnoreStack,
172
+ ignore,
184
173
  });
185
174
  return relativePath + '/';
186
175
  });
@@ -0,0 +1,24 @@
1
+ /**
2
+ *patterns describing which files should e comsidered as ignore files when creating the file tree.
3
+ *
4
+ * Format: one pattern per line, in gitignore format as defimed by https://git-scm.com/docs/gitignore#_pattern_format
5
+ */
6
+ export const ignoreFilePattern =
7
+ process.env.IGNORE_FILE_PATTERN ??
8
+ `
9
+ .vzignore
10
+ .ignore
11
+ .gitignore
12
+ `;
13
+
14
+ /**
15
+ * the base ignore patterns, that should be applied before any ignore file found when creating the file tree.
16
+ *
17
+ * These patterns apply even in the absence of an ignore file.
18
+ */
19
+ export const baseIgnore =
20
+ process.env.BASE_IGNORE ??
21
+ `
22
+ .git/
23
+ node_modules/
24
+ `;
@@ -1,4 +1,10 @@
1
1
  // Feature flag for directories, disabled until it's fully working.
2
2
  export const enableDirectories = true;
3
3
 
4
- export const debugDirectories = false;
4
+ export const debugDirectories = Boolean(
5
+ process.env.DEBUG_FILE_TREE,
6
+ );
7
+
8
+ export const debugIgnore = Boolean(
9
+ process.env.DEBUG_IGNORE,
10
+ );
@@ -0,0 +1,101 @@
1
+ import OpenAI from 'openai';
2
+ import { json1Presence } from '../ot.js';
3
+
4
+ const { editOp, type } = json1Presence;
5
+
6
+ const debug = false;
7
+
8
+ // Feature flag to slow down AI for development/testing
9
+ const slowdown = false;
10
+
11
+ let openai;
12
+ if (process.env.OPENAI_API_KEY !== undefined) {
13
+ openai = new OpenAI();
14
+ }
15
+
16
+ const AISourceName = 'AIAssist';
17
+
18
+ function opComesFromAIAssist(ops, source) {
19
+ return source === AISourceName;
20
+ }
21
+
22
+ const streams = {};
23
+
24
+ export const generateAIResponse = async ({
25
+ inputText,
26
+ insertionCursor,
27
+ fileId,
28
+ streamId,
29
+ shareDBDoc,
30
+ }) => {
31
+ if (debug) {
32
+ console.log(
33
+ '[generateAIResponse] inputText:',
34
+ inputText,
35
+ );
36
+ console.log(
37
+ '[generateAIResponse] insertionCursor:',
38
+ insertionCursor,
39
+ );
40
+ console.log('[generateAIResponse] fileId:', fileId);
41
+ console.log('[generateAIResponse] streamId:', streamId);
42
+ console.log(
43
+ '[generateAIResponse] shareDBDoc:',
44
+ shareDBDoc,
45
+ );
46
+ }
47
+ function accomodateDocChanges(op, source) {
48
+ if (!opComesFromAIAssist(op, source)) {
49
+ if (op !== null) {
50
+ insertionCursor = type
51
+ .transformPosition(
52
+ ['files', fileId, 'text', insertionCursor],
53
+ op,
54
+ )
55
+ .slice(-1)[0];
56
+ }
57
+ }
58
+ }
59
+
60
+ shareDBDoc.on('op', accomodateDocChanges);
61
+
62
+ streams[streamId] = await openai.chat.completions.create({
63
+ // model: 'gpt-3.5-turbo',
64
+ model: 'gpt-4',
65
+
66
+ messages: [
67
+ {
68
+ role: 'system',
69
+ content:
70
+ 'Write typescript or javascript code to continue the current file, given the other files for context. Use // for comments.',
71
+ },
72
+ { role: 'user', content: inputText },
73
+ ],
74
+ stream: true,
75
+ });
76
+
77
+ for await (const part of streams[streamId]) {
78
+ const op = editOp(
79
+ ['files', fileId, 'text'],
80
+ 'text-unicode',
81
+ [
82
+ insertionCursor,
83
+ part.choices[0]?.delta?.content || '',
84
+ ],
85
+ );
86
+
87
+ shareDBDoc.submitOp(op, { source: AISourceName });
88
+
89
+ // Wait for 500ms
90
+ if (slowdown) {
91
+ await new Promise((resolve) => {
92
+ setTimeout(resolve, 1000);
93
+ });
94
+ }
95
+
96
+ insertionCursor += (
97
+ part.choices[0]?.delta?.content || ''
98
+ ).length;
99
+ }
100
+ shareDBDoc.off('op', accomodateDocChanges);
101
+ };
@@ -1,109 +1,20 @@
1
- import OpenAI from 'openai';
2
- import { json1Presence } from '../ot.js';
1
+ import { generateAIResponse } from './generateAIResponse.js';
3
2
 
4
- const { editOp, type, replaceOp } = json1Presence;
3
+ const debug = false;
5
4
 
6
- // Feature flag to slow down AI for development/testing
7
- const slowdown = false;
8
-
9
- let openai;
10
- if (process.env.OPENAI_API_KEY !== undefined) {
11
- openai = new OpenAI();
12
- }
13
-
14
- export async function generateAIResponse({
15
- inputText,
16
- insertionCursor,
17
- fileId,
18
- streamId,
19
- shareDBDoc,
20
- }) {
21
- function accomodateDocChanges(op, source) {
22
- if (!opComesFromAIAssist(op, source)) {
23
- if (op !== null) {
24
- insertionCursor = type
25
- .transformPosition(
26
- ['files', fileId, 'text', insertionCursor],
27
- op,
28
- )
29
- .slice(-1)[0];
30
- }
31
- }
32
- }
33
-
34
- shareDBDoc.on('op', accomodateDocChanges);
35
-
36
- streams[streamId] = await openai.chat.completions.create({
37
- // model: 'gpt-3.5-turbo',
38
- model: 'gpt-4',
39
-
40
- messages: [
41
- {
42
- role: 'system',
43
- content:
44
- 'Write typescript or javascript code to continue the current file, given the other files for context. Use // for comments.',
45
- },
46
- { role: 'user', content: inputText },
47
- ],
48
- stream: true,
49
- });
5
+ export const handleAIAssist =
6
+ (shareDBDoc) => async (req, res) => {
7
+ const { inputText, insertionCursor, fileId } = req.body;
50
8
 
51
- for await (const part of streams[streamId]) {
52
- const op = editOp(
53
- ['files', fileId, 'text'],
54
- 'text-unicode',
55
- [
9
+ if (debug) {
10
+ console.log('[handleAIAssist] inputText:', inputText);
11
+ console.log(
12
+ '[handleAIAssist] insertionCursor:',
56
13
  insertionCursor,
57
- part.choices[0]?.delta?.content || '',
58
- ],
59
- );
60
-
61
- shareDBDoc.submitOp(op, { source: AISourceName });
62
-
63
- // Wait for 500ms
64
- if (slowdown) {
65
- await new Promise((resolve) => {
66
- setTimeout(resolve, 1000);
67
- });
14
+ );
15
+ console.log('[handleAIAssist] fileId:', fileId);
68
16
  }
69
17
 
70
- insertionCursor += (
71
- part.choices[0]?.delta?.content || ''
72
- ).length;
73
- }
74
- shareDBDoc.off('op', accomodateDocChanges);
75
-
76
- const confirmCompletedOperation = replaceOp(
77
- [
78
- 'aiStreams',
79
- streamId,
80
- 'AIStreamStatus',
81
- 'serverIsRunning',
82
- ],
83
- true,
84
- false,
85
- );
86
- shareDBDoc.submitOp(confirmCompletedOperation, {
87
- source: 'AIServer',
88
- });
89
- }
90
-
91
- const AISourceName = 'AIAssist';
92
-
93
- function opComesFromAIAssist(ops, source) {
94
- return source === AISourceName;
95
- }
96
-
97
- const streams = {};
98
-
99
- export const handleAIAssist =
100
- (shareDBDoc) => async (req, res) => {
101
- const {
102
- text: inputText,
103
- cursorLocation: insertionCursor,
104
- fileId,
105
- } = req.body;
106
-
107
18
  try {
108
19
  await generateAIResponse({
109
20
  inputText,
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import './setupEnv.js';
2
3
  import http from 'http';
3
4
  import express from 'express';
4
5
  import ShareDB from 'sharedb';
@@ -9,19 +10,10 @@ import path from 'path';
9
10
  import { fileURLToPath } from 'url';
10
11
  import open from 'open';
11
12
  import ngrok from 'ngrok';
12
- import dotenv from 'dotenv';
13
13
  import bodyParser from 'body-parser';
14
14
  import { json1Presence } from '../ot.js';
15
15
  import { computeInitialDocument } from './computeInitialDocument.js';
16
- import {
17
- generateAIResponse,
18
- haltGeneration,
19
- handleAIAssist,
20
- } from './handleAIAssist.js';
21
-
22
- const { replaceOp } = json1Presence;
23
-
24
- dotenv.config({ path: '../../.env' });
16
+ import { handleAIAssist } from './handleAIAssist.js';
25
17
 
26
18
  // The time in milliseconds by which auto-saving is debounced.
27
19
  const autoSaveDebounceTimeMS = 800;
@@ -105,7 +97,7 @@ shareDBDoc.create(initialDocument, json1Presence.type.uri);
105
97
 
106
98
  // Handle AI Assist requests.
107
99
  app.post(
108
- '/AIAssist',
100
+ '/ai-assist',
109
101
  bodyParser.json(),
110
102
  handleAIAssist(shareDBDoc),
111
103
  );
@@ -221,53 +213,53 @@ shareDBDoc.subscribe(() => {
221
213
  debounceSave();
222
214
  }
223
215
 
224
- if (
225
- op !== null &&
226
- op[0] == 'aiStreams' &&
227
- source !== 'AIServer' &&
228
- source !== 'AIAssist'
229
- ) {
230
- //This is an insert operation (a new request)
231
- if (
232
- op[op.length - 1]['i'] !== undefined &&
233
- op[op.length - 1]['i']['AIStreamStatus'] !==
234
- undefined
235
- ) {
236
- const input =
237
- op[op.length - 1]['i']['AIStreamStatus'];
238
-
239
- generateAIResponse({
240
- inputText: input.text,
241
- insertionCursor: input.insertionCursor,
242
- shareDBDoc: shareDBDoc,
243
- streamId: op[op.length - 2],
244
- fileId: input.fileId,
245
- });
246
-
247
- const confirmStartOperation = replaceOp(
248
- [
249
- ...op.filter(
250
- (value) => typeof value === 'string',
251
- ),
252
- 'AIStreamStatus',
253
- 'serverIsRunning',
254
- ],
255
- true,
256
- true,
257
- );
258
-
259
- shareDBDoc.submitOp(confirmStartOperation, {
260
- source: 'AIServer',
261
- });
262
- }
263
- if (
264
- op[op.length - 1]['r'] !== null &&
265
- op[op.length - 2] == 'clientWantsToStart'
266
- ) {
267
- //client wants to halt generation
268
- haltGeneration(op[1]);
269
- }
270
- }
216
+ // if (
217
+ // op !== null &&
218
+ // op[0] == 'aiStreams' &&
219
+ // source !== 'AIServer' &&
220
+ // source !== 'AIAssist'
221
+ // ) {
222
+ // //This is an insert operation (a new request)
223
+ // if (
224
+ // op[op.length - 1]['i'] !== undefined &&
225
+ // op[op.length - 1]['i']['AIStreamStatus'] !==
226
+ // undefined
227
+ // ) {
228
+ // const input =
229
+ // op[op.length - 1]['i']['AIStreamStatus'];
230
+
231
+ // generateAIResponse({
232
+ // inputText: input.text,
233
+ // insertionCursor: input.insertionCursor,
234
+ // shareDBDoc: shareDBDoc,
235
+ // streamId: op[op.length - 2],
236
+ // fileId: input.fileId,
237
+ // });
238
+
239
+ // const confirmStartOperation = replaceOp(
240
+ // [
241
+ // ...op.filter(
242
+ // (value) => typeof value === 'string',
243
+ // ),
244
+ // 'AIStreamStatus',
245
+ // 'serverIsRunning',
246
+ // ],
247
+ // true,
248
+ // true,
249
+ // );
250
+
251
+ // shareDBDoc.submitOp(confirmStartOperation, {
252
+ // source: 'AIServer',
253
+ // });
254
+ // }
255
+ // if (
256
+ // op[op.length - 1]['r'] !== null &&
257
+ // op[op.length - 2] == 'clientWantsToStart'
258
+ // ) {
259
+ // //client wants to halt generation
260
+ // haltGeneration(op[1]);
261
+ // }
262
+ // }
271
263
  });
272
264
  });
273
265
 
@@ -0,0 +1,18 @@
1
+ import dotenv from 'dotenv';
2
+ import { fileURLToPath } from 'url';
3
+ import { join } from 'node:path';
4
+
5
+ const dir = fileURLToPath(import.meta.url);
6
+ dotenv.config({
7
+ path: join(dir, '../../../.env'),
8
+ });
9
+ if (process.env.DEBUG_SETUP_ENV) {
10
+ console.log(
11
+ 'environment variables set up',
12
+ Object.fromEntries(
13
+ Object.entries(process.env).filter(
14
+ (entry) => !entry[0].endsWith('PATH'),
15
+ ),
16
+ ),
17
+ );
18
+ }
package/src/types.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  // FileId
2
2
  // * A unique ID for a file.
3
3
 
4
- import { JSONOp } from 'ot-json1';
5
-
6
4
  // * This is a random string.
7
5
  export type FileId = string;
8
6
 
@@ -60,6 +58,8 @@ export interface FileTreeFile {
60
58
  fileId: FileId;
61
59
  }
62
60
 
61
+ export type JSONOp = any;
62
+
63
63
  // `ShareDBDoc`
64
64
  // A ShareDB Document.
65
65
  // TODO better types? Integrate with upstream how?
@@ -87,23 +87,23 @@ export type ShareDBDoc<T> = {
87
87
  // Generated by the client.
88
88
  export type AIStreamId = string;
89
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;
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
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;
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
101
 
102
- text: string;
102
+ // text: string;
103
103
 
104
- startingInsertionCursor: number;
105
- fileId: FileId;
106
- };
104
+ // startingInsertionCursor: number;
105
+ // fileId: FileId;
106
+ // };
107
107
 
108
108
  /// Alternative universe
109
109
 
@@ -142,7 +142,7 @@ export type AIStreamStatus = {
142
142
  export type VZCodeContent = {
143
143
  // `files`
144
144
  // * The files in the VZCode instance.
145
- files: Files;
145
+ files?: Files;
146
146
 
147
147
  // `isInteracting`
148
148
  // * `true` when the user is interacting
@@ -161,12 +161,12 @@ export type VZCodeContent = {
161
161
  // client and server.
162
162
  // * This can power the transition of the icon from
163
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
- };
164
+ // aiStreams?: {
165
+ // // These need to be removed at some point?
166
+ // [aiStreamId: AIStreamId]: {
167
+ // AIStreamStatus: AIStreamStatus;
168
+ // };
169
+ // };
170
170
 
171
171
  // TODO consider is there a way to replace the HTTP request to
172
172
  // the AIAssist endpoint with a manipulation of the ShareDB document?