vzcode 0.26.0 → 0.28.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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-a9585cbc.js +111 -0
  3. package/dist/assets/index-d30c9f1b.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +6 -4
  6. package/src/client/AIAssist.ts +46 -0
  7. package/src/client/App.tsx +86 -43
  8. package/src/client/CodeEditor/getOrCreateEditor.ts +22 -2
  9. package/src/client/CodeEditor/index.tsx +75 -12
  10. package/src/client/CodeEditor/json1PresenceBroadcast.ts +6 -2
  11. package/src/client/CodeEditor/json1PresenceDisplay.ts +0 -45
  12. package/src/client/CodeEditor/style.scss +5 -1
  13. package/src/client/CodeErrorOverlay/index.tsx +37 -0
  14. package/src/client/CodeErrorOverlay/style.scss +41 -0
  15. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  16. package/src/client/PresenceNotifications/index.tsx +118 -103
  17. package/src/client/SplitPaneResizeContext.tsx +12 -7
  18. package/src/client/TabList/Tab.tsx +20 -6
  19. package/src/client/TabList/index.tsx +18 -8
  20. package/src/client/TabList/style.scss +5 -0
  21. package/src/client/VZSettings.tsx +33 -20
  22. package/src/client/VZSidebar/{ConfirmationBox.tsx → DeleteConfirmationModal.tsx} +17 -14
  23. package/src/client/VZSidebar/DirectoryListing.tsx +27 -26
  24. package/src/client/VZSidebar/FileListing.tsx +16 -19
  25. package/src/client/VZSidebar/Item.tsx +83 -37
  26. package/src/client/VZSidebar/Listing.tsx +17 -14
  27. package/src/client/VZSidebar/index.tsx +87 -45
  28. package/src/client/VZSidebar/styles.scss +21 -1
  29. package/src/client/style.scss +3 -0
  30. package/src/client/useActions.ts +25 -6
  31. package/src/client/useEditorCache.ts +2 -1
  32. package/src/client/useFileCRUD.ts +29 -27
  33. package/src/client/usernameLocalStorage.ts +43 -0
  34. package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
  35. package/src/client/vzReducer/closeTabsReducer.ts +68 -0
  36. package/src/client/vzReducer/createInitialState.ts +19 -0
  37. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  38. package/src/client/vzReducer/index.ts +107 -0
  39. package/src/client/vzReducer/openTabReducer.test.ts +110 -0
  40. package/src/client/vzReducer/openTabReducer.ts +57 -0
  41. package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
  42. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  43. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  44. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  45. package/src/index.ts +9 -3
  46. package/src/server/handleAIAssist.js +69 -0
  47. package/src/server/index.js +9 -0
  48. package/src/types.ts +12 -5
  49. package/dist/assets/index-284b7339.js +0 -111
  50. package/dist/assets/index-e160f758.css +0 -1
  51. package/src/client/PrettierErrorOverlay/index.tsx +0 -9
  52. package/src/client/PrettierErrorOverlay/style.scss +0 -9
  53. package/src/client/vzReducer.ts +0 -118
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from 'react';
2
2
  import { ThemeLabel } from './themes';
3
- import { FileId } from '../types';
4
- import { VZAction } from './vzReducer';
3
+ import { FileId, Username } from '../types';
4
+ import { TabState, VZAction } from './vzReducer';
5
5
 
6
6
  // This is a custom hook that returns a set of functions
7
7
  // that can be used to dispatch actions to the reducer.
@@ -19,20 +19,21 @@ export const useActions = (
19
19
  );
20
20
 
21
21
  const openTab = useCallback(
22
- (fileId: FileId) => {
22
+ (tabState: TabState): void => {
23
23
  dispatch({
24
24
  type: 'open_tab',
25
- fileId,
25
+ fileId: tabState.fileId,
26
+ isTransient: tabState.isTransient,
26
27
  });
27
28
  },
28
29
  [dispatch],
29
30
  );
30
31
 
31
32
  const closeTabs = useCallback(
32
- (idsToDelete: Array<FileId>) => {
33
+ (fileIdsToClose: Array<FileId>) => {
33
34
  dispatch({
34
35
  type: 'close_tabs',
35
- idsToDelete,
36
+ fileIdsToClose,
36
37
  });
37
38
  },
38
39
  [dispatch],
@@ -63,6 +64,22 @@ export const useActions = (
63
64
  setIsSettingsOpen(false);
64
65
  }, [setIsSettingsOpen]);
65
66
 
67
+ const editorNoLongerWantsFocus = useCallback(() => {
68
+ dispatch({
69
+ type: 'editor_no_longer_wants_focus',
70
+ });
71
+ }, [dispatch]);
72
+
73
+ const setUsername = useCallback(
74
+ (username: Username) => {
75
+ dispatch({
76
+ type: 'set_username',
77
+ username,
78
+ });
79
+ },
80
+ [dispatch],
81
+ );
82
+
66
83
  return {
67
84
  setActiveFileId,
68
85
  openTab,
@@ -70,5 +87,7 @@ export const useActions = (
70
87
  setTheme,
71
88
  setIsSettingsOpen,
72
89
  closeSettings,
90
+ editorNoLongerWantsFocus,
91
+ setUsername,
73
92
  };
74
93
  };
@@ -1,8 +1,9 @@
1
1
  import { useRef } from 'react';
2
2
  import { FileId } from '../types';
3
+ import { EditorView } from 'codemirror';
3
4
 
4
5
  export type EditorCacheValue = {
5
- editor: any;
6
+ editor: EditorView;
6
7
  themeCompartment: any;
7
8
  };
8
9
 
@@ -1,5 +1,9 @@
1
1
  import { useCallback } from 'react';
2
- import { FileId, VZCodeContent } from '../types';
2
+ import {
3
+ FileId,
4
+ FileTreePath,
5
+ VZCodeContent,
6
+ } from '../types';
3
7
  import { randomId } from '../randomId';
4
8
 
5
9
  // CRUD operations for files and directories
@@ -7,6 +11,19 @@ import { randomId } from '../randomId';
7
11
  export const useFileCRUD = ({
8
12
  submitOperation,
9
13
  closeTabs,
14
+ openTab,
15
+ }: {
16
+ submitOperation: (
17
+ operation: (document: VZCodeContent) => VZCodeContent,
18
+ ) => void;
19
+ closeTabs: (idsToDelete: Array<FileId>) => void;
20
+ openTab: ({
21
+ fileId,
22
+ isTransient,
23
+ }: {
24
+ fileId: FileId;
25
+ isTransient: boolean;
26
+ }) => void;
10
27
  }) => {
11
28
  // Create a new file
12
29
  const createFile = useCallback(() => {
@@ -14,20 +31,24 @@ export const useFileCRUD = ({
14
31
  // See https://github.com/vizhub-core/vzcode/issues/252
15
32
  const name = prompt('Enter new file name');
16
33
  if (name) {
34
+ const fileId: FileId = randomId();
17
35
  submitOperation((document: VZCodeContent) => ({
18
36
  ...document,
19
37
  files: {
20
38
  ...document.files,
21
- [randomId()]: { name, text: '' },
39
+ [fileId]: { name, text: '' },
22
40
  },
23
41
  }));
42
+ // When a new file is created, open it in a new tab
43
+ // and focus the editor on it.
44
+ openTab({ fileId, isTransient: false });
24
45
  }
25
46
  }, [submitOperation]);
26
47
 
27
48
  // Called when a file in the sidebar is renamed.
28
- const handleRenameFileClick = useCallback(
49
+ const renameFile = useCallback(
29
50
  (fileId: FileId, newName: string) => {
30
- submitOperation((document) => ({
51
+ submitOperation((document: VZCodeContent) => ({
31
52
  ...document,
32
53
  files: {
33
54
  ...document.files,
@@ -56,7 +77,7 @@ export const useFileCRUD = ({
56
77
 
57
78
  // Deletes a directory and all files within it
58
79
  const deleteDirectory = useCallback(
59
- (path: FileId) => {
80
+ (path: FileTreePath) => {
60
81
  const tabsToClose: Array<FileId> = [];
61
82
  submitOperation((document: VZCodeContent) => {
62
83
  const updatedFiles = { ...document.files };
@@ -73,29 +94,10 @@ export const useFileCRUD = ({
73
94
  [submitOperation, closeTabs],
74
95
  );
75
96
 
76
- // Generic delete function that calls either deleteFile or deleteDirectory
77
- // TODO consider if there is a cleaner way to do this
78
- // - Ideally we would have a separate function for deleting files and directories
79
- // - When we click the delete button, we should be able to tell if it is a file or directory
80
- const handleDeleteClick = useCallback(
81
- (key: string) => {
82
- // Regex to identify if the key is a file path or a file id.
83
- if (/^[0-9]*$/.test(key)) {
84
- if (key.length == 8) {
85
- deleteFile(key);
86
- } else {
87
- deleteDirectory(key);
88
- }
89
- } else {
90
- deleteDirectory(key);
91
- }
92
- },
93
- [deleteFile, deleteDirectory],
94
- );
95
-
96
97
  return {
97
98
  createFile,
98
- handleRenameFileClick,
99
- handleDeleteClick,
99
+ renameFile,
100
+ deleteFile,
101
+ deleteDirectory,
100
102
  };
101
103
  };
@@ -0,0 +1,43 @@
1
+ // This module deals with storing and accessing the
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { Username } from '../types';
5
+
6
+ const localStoragePropertyName = 'vzCodeUsername';
7
+
8
+ const initialUsernameDefault = 'Anonymous';
9
+
10
+ // Gets the username from local storage.
11
+ export const useInitialUsername = (): Username => {
12
+ let initialUsername: Username = initialUsernameDefault;
13
+
14
+ // If we're in the browser,
15
+ if (typeof window !== 'undefined') {
16
+ //check localStorage for a previously stored width.
17
+ const initialUsernameFromLocalStorage: string | null =
18
+ window.localStorage.getItem(localStoragePropertyName);
19
+
20
+ // If there is a previously stored width,
21
+ if (initialUsernameFromLocalStorage !== null) {
22
+ // use it as the initial width.
23
+ initialUsername = initialUsernameFromLocalStorage;
24
+ }
25
+ } else {
26
+ // If we're not in the browser, use the default initial width.
27
+ }
28
+ return initialUsername;
29
+ };
30
+
31
+ // Stores the username to local storage.
32
+ export const usePersistUsername = (username: Username) => {
33
+ const previousUsername = useRef<Username>(username);
34
+ useEffect(() => {
35
+ if (previousUsername.current !== username) {
36
+ previousUsername.current = username;
37
+ window.localStorage.setItem(
38
+ 'vzCodeUsername',
39
+ username,
40
+ );
41
+ }
42
+ }, [username]);
43
+ };
@@ -0,0 +1,177 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { closeTabsReducer } from './closeTabsReducer';
3
+ import { VZAction, VZState, createInitialState } from '.';
4
+ import { defaultTheme } from '../themes';
5
+
6
+ const initialState: VZState = createInitialState({
7
+ defaultTheme,
8
+ });
9
+
10
+ describe('closeTabsReducer', () => {
11
+ it('Closes an existing tab', () => {
12
+ const action: VZAction = {
13
+ type: 'close_tabs',
14
+ fileIdsToClose: ['file1'],
15
+ };
16
+
17
+ const stateWithTabs = {
18
+ ...initialState,
19
+ tabList: [{ fileId: 'file1', isTransient: false }],
20
+ activeFileId: 'file1',
21
+ };
22
+
23
+ const newState = closeTabsReducer(
24
+ stateWithTabs,
25
+ action,
26
+ );
27
+
28
+ expect(newState.tabList.length).toBe(0);
29
+ expect(newState.activeFileId).toBeNull();
30
+ });
31
+
32
+ it('Keeps other tabs open', () => {
33
+ const action: VZAction = {
34
+ type: 'close_tabs',
35
+ fileIdsToClose: ['file2'],
36
+ };
37
+ const stateWithTabs = {
38
+ ...initialState,
39
+ tabList: [
40
+ { fileId: 'file1', isTransient: false },
41
+ { fileId: 'file2', isTransient: false },
42
+ ],
43
+ activeFileId: 'file1',
44
+ };
45
+
46
+ const newState = closeTabsReducer(
47
+ stateWithTabs,
48
+ action,
49
+ );
50
+
51
+ expect(newState.tabList.length).toBe(1);
52
+ expect(newState.tabList[0].fileId).toBe('file1');
53
+ expect(newState.activeFileId).toBe('file1');
54
+ });
55
+
56
+ it('Activates previous tab after closing an active tab', () => {
57
+ const action: VZAction = {
58
+ type: 'close_tabs',
59
+ fileIdsToClose: ['file2'],
60
+ };
61
+ const stateWithTabs = {
62
+ ...initialState,
63
+ tabList: [
64
+ { fileId: 'file1', isTransient: false },
65
+ { fileId: 'file2', isTransient: false },
66
+ { fileId: 'file3', isTransient: false },
67
+ ],
68
+ activeFileId: 'file2',
69
+ };
70
+
71
+ const newState = closeTabsReducer(
72
+ stateWithTabs,
73
+ action,
74
+ );
75
+
76
+ expect(newState.tabList.length).toBe(2);
77
+ expect(newState.activeFileId).toBe('file1'); // should switch to the previous tab
78
+ });
79
+
80
+ it('Does not modify the state if closing non-existing tabs', () => {
81
+ const action: VZAction = {
82
+ type: 'close_tabs',
83
+ fileIdsToClose: ['file4'],
84
+ };
85
+ const stateWithTabs = {
86
+ ...initialState,
87
+ tabList: [
88
+ { fileId: 'file1', isTransient: false },
89
+ { fileId: 'file2', isTransient: false },
90
+ { fileId: 'file3', isTransient: false },
91
+ ],
92
+ activeFileId: 'file2',
93
+ };
94
+
95
+ const newState = closeTabsReducer(
96
+ stateWithTabs,
97
+ action,
98
+ );
99
+
100
+ expect(newState).toEqual(stateWithTabs);
101
+ });
102
+
103
+ it('Closes a tab that is not currently active', () => {
104
+ const stateWithMultipleTabs = {
105
+ ...initialState,
106
+ tabList: [
107
+ { fileId: 'file1', isTransient: false },
108
+ { fileId: 'file2', isTransient: false },
109
+ { fileId: 'file3', isTransient: false },
110
+ ],
111
+ activeFileId: 'file3',
112
+ };
113
+
114
+ const action: VZAction = {
115
+ type: 'close_tabs',
116
+ fileIdsToClose: ['file2'],
117
+ };
118
+
119
+ const newState = closeTabsReducer(
120
+ stateWithMultipleTabs,
121
+ action,
122
+ );
123
+
124
+ expect(newState.tabList.length).toBe(2);
125
+ expect(newState.activeFileId).toBe('file3'); // remains unchanged
126
+ });
127
+
128
+ it('Closes the first tab out of many, when it is active', () => {
129
+ const stateWithMultipleTabs = {
130
+ ...initialState,
131
+ tabList: [
132
+ { fileId: 'file1', isTransient: false },
133
+ { fileId: 'file2', isTransient: false },
134
+ { fileId: 'file3', isTransient: false },
135
+ ],
136
+ activeFileId: 'file1',
137
+ };
138
+
139
+ const action: VZAction = {
140
+ type: 'close_tabs',
141
+ fileIdsToClose: ['file1'],
142
+ };
143
+
144
+ const newState = closeTabsReducer(
145
+ stateWithMultipleTabs,
146
+ action,
147
+ );
148
+
149
+ expect(newState.tabList.length).toBe(2);
150
+ expect(newState.activeFileId).toBe('file2'); // switches to the next available tab
151
+ });
152
+
153
+ it('Closes multiple tabs at once', () => {
154
+ const stateWithMultipleTabs = {
155
+ ...initialState,
156
+ tabList: [
157
+ { fileId: 'file1', isTransient: false },
158
+ { fileId: 'file2', isTransient: false },
159
+ { fileId: 'file3', isTransient: false },
160
+ ],
161
+ activeFileId: 'file2',
162
+ };
163
+
164
+ const action: VZAction = {
165
+ type: 'close_tabs',
166
+ fileIdsToClose: ['file1', 'file2'],
167
+ };
168
+
169
+ const newState = closeTabsReducer(
170
+ stateWithMultipleTabs,
171
+ action,
172
+ );
173
+
174
+ expect(newState.tabList.length).toBe(1);
175
+ expect(newState.activeFileId).toBe('file3'); // switches to the next available tab
176
+ });
177
+ });
@@ -0,0 +1,68 @@
1
+ import { TabState, VZAction, VZState } from '.';
2
+ import { FileId } from '../../types';
3
+
4
+ export const closeTabsReducer = (
5
+ state: VZState,
6
+ action: VZAction,
7
+ ): VZState => {
8
+ if (action.type !== 'close_tabs') {
9
+ return state;
10
+ }
11
+
12
+ const newTabList: Array<TabState> = state.tabList.filter(
13
+ (tabState) =>
14
+ !action.fileIdsToClose.includes(tabState.fileId),
15
+ );
16
+
17
+ // If the active tab wasn't closed, keep it active.
18
+ if (
19
+ !action.fileIdsToClose.includes(state.activeFileId!)
20
+ ) {
21
+ return {
22
+ ...state,
23
+ tabList: newTabList,
24
+ };
25
+ }
26
+
27
+ const originalIndex = state.tabList.findIndex(
28
+ (tabState) => tabState.fileId === state.activeFileId,
29
+ );
30
+
31
+ let newActiveFileId: FileId | null = null;
32
+
33
+ // Try getting the previous available tab first.
34
+ for (let i = originalIndex - 1; i >= 0; i--) {
35
+ if (
36
+ !action.fileIdsToClose.includes(
37
+ state.tabList[i].fileId,
38
+ )
39
+ ) {
40
+ newActiveFileId = state.tabList[i].fileId;
41
+ break;
42
+ }
43
+ }
44
+
45
+ // If no previous tab available, get the next available one.
46
+ if (!newActiveFileId) {
47
+ for (
48
+ let i = originalIndex + 1;
49
+ i < state.tabList.length;
50
+ i++
51
+ ) {
52
+ if (
53
+ !action.fileIdsToClose.includes(
54
+ state.tabList[i].fileId,
55
+ )
56
+ ) {
57
+ newActiveFileId = state.tabList[i].fileId;
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ return {
64
+ ...state,
65
+ tabList: newTabList,
66
+ activeFileId: newActiveFileId,
67
+ };
68
+ };
@@ -0,0 +1,19 @@
1
+ import { VZState } from '.';
2
+ import { ThemeLabel } from '../themes';
3
+
4
+ export const createInitialState = ({
5
+ defaultTheme,
6
+ initialUsername,
7
+ }: {
8
+ defaultTheme: ThemeLabel;
9
+ initialUsername?: string;
10
+ }): VZState => ({
11
+ tabList: [],
12
+ activeFileId: null,
13
+ theme: defaultTheme,
14
+ isSettingsOpen: false,
15
+ editorWantsFocus: false,
16
+
17
+ // TODO unify this with App.tsx
18
+ username: initialUsername || 'Anonymous',
19
+ });
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const editorNoLongerWantsFocusReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'editor_no_longer_wants_focus'
8
+ ? { ...state, editorWantsFocus: false }
9
+ : state;
@@ -0,0 +1,107 @@
1
+ import { FileId, Username } from '../../types';
2
+ import { ThemeLabel } from '../themes';
3
+ import { closeTabsReducer } from './closeTabsReducer';
4
+ import { openTabReducer } from './openTabReducer';
5
+ import { setActiveFileIdReducer } from './setActiveFileIdReducer';
6
+ import { setIsSettingsOpenReducer } from './setIsSettingsOpenReducer';
7
+ import { setThemeReducer } from './setThemeReducer';
8
+ import { editorNoLongerWantsFocusReducer } from './editorNoLongerWantsFocusReducer';
9
+ import { setUsernameReducer } from './setUsernameReducer';
10
+
11
+ export { createInitialState } from './createInitialState';
12
+
13
+ // The shape of the state managed by the reducer.
14
+ export type VZState = {
15
+ // The list of open tabs.
16
+ tabList: Array<TabState>;
17
+
18
+ // The ID of the file that is currently active.
19
+ // Invariant: `activeFileId` is always in `tabList`.
20
+ activeFileId: FileId | null;
21
+
22
+ // The theme that is currently active.
23
+ theme: ThemeLabel;
24
+
25
+ // True to show the settings modal.
26
+ isSettingsOpen: boolean;
27
+
28
+ // True if the editor should focus on the next render.
29
+ editorWantsFocus: boolean;
30
+
31
+ // The username of the current user.
32
+ username: Username;
33
+ };
34
+
35
+ // The shape of the actions that can be dispatched to the reducer.
36
+ export type VZAction =
37
+ // `set_active_file_id`
38
+ // * Sets the active file ID.
39
+ | { type: 'set_active_file_id'; activeFileId: FileId }
40
+
41
+ // `open_tab`
42
+ // * Opens a tab.
43
+ // * Also serves to change an already open transient tab to persistent.
44
+ | {
45
+ type: 'open_tab';
46
+ fileId: FileId;
47
+ isTransient: boolean;
48
+ }
49
+
50
+ // `close_tabs`
51
+ // * Closes a set of tabs.
52
+ | { type: 'close_tabs'; fileIdsToClose: Array<FileId> }
53
+
54
+ // `set_theme`
55
+ // * Sets the theme.
56
+ | { type: 'set_theme'; themeLabel: ThemeLabel }
57
+
58
+ // `set_is_settings_open`
59
+ // * Sets whether the settings modal is open.
60
+ | { type: 'set_is_settings_open'; value: boolean }
61
+
62
+ // `editor_no_longer_wants_focus`
63
+ // * Sets `editorWantsFocus` to `false`.
64
+ | { type: 'editor_no_longer_wants_focus' }
65
+
66
+ // `set_username`
67
+ // * Sets the username.
68
+ | { type: 'set_username'; username: Username };
69
+
70
+ // Representation of an open tab.
71
+ export type TabState = {
72
+ // `fileId`
73
+ // The ID of the file that the tab represents.
74
+ fileId: FileId;
75
+
76
+ // `isTransient`
77
+ // Represents whether the tab is temporary or persistent.
78
+ // * `true` if the tab is temporary, meaning its text
79
+ // appears as italic, and it will be automatically
80
+ // closed when the user switches to another file.
81
+ // If `true` and the tab is opened, the editor will not focus.
82
+ // * `false` if the tab is persistent, meaning its text
83
+ // appears as normal, and it will not be automatically
84
+ // closed when the user switches to another file.
85
+ // If `false` and the tab is opened, the editor will focus.
86
+ isTransient: boolean;
87
+ };
88
+
89
+ const reducers = [
90
+ setActiveFileIdReducer,
91
+ openTabReducer,
92
+ closeTabsReducer,
93
+ setThemeReducer,
94
+ setIsSettingsOpenReducer,
95
+ editorNoLongerWantsFocusReducer,
96
+ setUsernameReducer,
97
+ ];
98
+
99
+ export const vzReducer = (
100
+ state: VZState,
101
+ action: VZAction,
102
+ ): VZState =>
103
+ reducers.reduce(
104
+ (currentState, currentReducer) =>
105
+ currentReducer(currentState, action),
106
+ state,
107
+ );