jupyterlab-chat 0.23.0 → 0.23.2

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { resolveChatRenamePath } from '../utils';
6
+ describe('resolveChatRenamePath', () => {
7
+ it('keeps a renamed chat in its original directory', () => {
8
+ expect(resolveChatRenamePath('chats/untitled.chat', 'test')).toBe('chats/test.chat');
9
+ });
10
+ it('ensures the .chat extension', () => {
11
+ expect(resolveChatRenamePath('chats/untitled.chat', 'test.chat')).toBe('chats/test.chat');
12
+ });
13
+ it('leaves root-level chats at the root', () => {
14
+ expect(resolveChatRenamePath('untitled.chat', 'test')).toBe('test.chat');
15
+ });
16
+ it('respects an explicit directory in the new name', () => {
17
+ expect(resolveChatRenamePath('chats/untitled.chat', 'archive/test')).toBe('archive/test.chat');
18
+ });
19
+ it('preserves nested directories', () => {
20
+ expect(resolveChatRenamePath('chats/2024/untitled.chat', 'test')).toBe('chats/2024/test.chat');
21
+ });
22
+ });
package/lib/model.d.ts CHANGED
@@ -64,6 +64,21 @@ export declare class LabChatModel extends AbstractChatModel implements DocumentR
64
64
  get readOnly(): boolean;
65
65
  set readOnly(value: boolean);
66
66
  set id(value: string | undefined);
67
+ /**
68
+ * Notify the model that its shared document has been synchronized with the
69
+ * server, and give it an ID if the document does not carry one yet.
70
+ *
71
+ * The model is only ready once it has an ID, so this must be called as soon as
72
+ * the document content is known - the ID cannot be created earlier, or a chat
73
+ * that already has one stored would end up with a conflicting ID.
74
+ *
75
+ * Callers should hook this to `IDocumentProvider.ready` (or the document
76
+ * context's `ready`) rather than to the document becoming clean: since
77
+ * jupyter-collaboration 5 a room stays dirty after being loaded, so the
78
+ * transition out of the dirty state may only happen on the first save, about a
79
+ * second later, leaving the chat unusable until then.
80
+ */
81
+ markDocumentSynced(): void;
67
82
  dispose(): void;
68
83
  toString(): string;
69
84
  fromString(data: string): void;
package/lib/model.js CHANGED
@@ -226,6 +226,10 @@ export class LabChatModel extends AbstractChatModel {
226
226
  });
227
227
  }
228
228
  // Create a chat ID if not created when the document is not dirty.
229
+ //
230
+ // This is a fallback for chats whose document is not backed by a
231
+ // collaborative provider, and so never reports being synchronized. When
232
+ // there is one, `markDocumentSynced()` gets there first.
229
233
  if (changes.stateChange && !this._sharedModel.id) {
230
234
  if (changes.stateChange.some(change => change.name === 'dirty' && !change.newValue)) {
231
235
  this._sharedModel.id = UUID.uuid4();
@@ -295,6 +299,27 @@ export class LabChatModel extends AbstractChatModel {
295
299
  this.setReady();
296
300
  }
297
301
  }
302
+ /**
303
+ * Notify the model that its shared document has been synchronized with the
304
+ * server, and give it an ID if the document does not carry one yet.
305
+ *
306
+ * The model is only ready once it has an ID, so this must be called as soon as
307
+ * the document content is known - the ID cannot be created earlier, or a chat
308
+ * that already has one stored would end up with a conflicting ID.
309
+ *
310
+ * Callers should hook this to `IDocumentProvider.ready` (or the document
311
+ * context's `ready`) rather than to the document becoming clean: since
312
+ * jupyter-collaboration 5 a room stays dirty after being loaded, so the
313
+ * transition out of the dirty state may only happen on the first save, about a
314
+ * second later, leaving the chat unusable until then.
315
+ */
316
+ markDocumentSynced() {
317
+ if (!this._sharedModel.id) {
318
+ // Assigning the shared ID emits a metadata change, which sets the model
319
+ // ID - and therefore resolves `ready` - through `_onchange`.
320
+ this._sharedModel.id = UUID.uuid4();
321
+ }
322
+ }
298
323
  dispose() {
299
324
  if (this.isDisposed) {
300
325
  return;
package/lib/utils.d.ts CHANGED
@@ -6,3 +6,17 @@
6
6
  * @returns - the display name of the chat.
7
7
  */
8
8
  export declare function getDisplayName(path: string, defaultDirectory?: string): string;
9
+ /**
10
+ * Resolve the target path for a chat rename.
11
+ *
12
+ * When the new name is a bare basename (the usual case when renaming via the
13
+ * dialog), the chat must stay in the same directory as the original file.
14
+ * Otherwise `Contents.rename` treats the name as root-relative and moves the
15
+ * chat out of its directory (e.g. the configured `defaultDirectory`).
16
+ *
17
+ * @param oldPath - the current path of the chat file (e.g. `chats/untitled.chat`).
18
+ * @param newName - the new name or path entered by the user (e.g. `test`).
19
+ * @returns - the resolved path, with the `.chat` extension ensured and the
20
+ * original directory preserved when `newName` has no directory component.
21
+ */
22
+ export declare function resolveChatRenamePath(oldPath: string, newName: string): string;
package/lib/utils.js CHANGED
@@ -22,3 +22,29 @@ export function getDisplayName(path, defaultDirectory) {
22
22
  : path
23
23
  : '/' + path).replace(pattern, '');
24
24
  }
25
+ /**
26
+ * Resolve the target path for a chat rename.
27
+ *
28
+ * When the new name is a bare basename (the usual case when renaming via the
29
+ * dialog), the chat must stay in the same directory as the original file.
30
+ * Otherwise `Contents.rename` treats the name as root-relative and moves the
31
+ * chat out of its directory (e.g. the configured `defaultDirectory`).
32
+ *
33
+ * @param oldPath - the current path of the chat file (e.g. `chats/untitled.chat`).
34
+ * @param newName - the new name or path entered by the user (e.g. `test`).
35
+ * @returns - the resolved path, with the `.chat` extension ensured and the
36
+ * original directory preserved when `newName` has no directory component.
37
+ */
38
+ export function resolveChatRenamePath(oldPath, newName) {
39
+ let newPath = newName;
40
+ // Ensure the `.chat` extension.
41
+ if (!newPath.endsWith(chatFileType.extensions[0])) {
42
+ newPath = `${newPath}${chatFileType.extensions[0]}`;
43
+ }
44
+ // Bare basename (no separator) -> keep it in the original file's directory.
45
+ // An explicit directory in `newName` is respected as-is.
46
+ if (!newPath.includes('/')) {
47
+ newPath = PathExt.join(PathExt.dirname(oldPath), newPath);
48
+ }
49
+ return newPath;
50
+ }
package/lib/widget.js CHANGED
@@ -27,6 +27,11 @@ export class LabChatPanel extends DocumentWidget {
27
27
  this.addClass(MAIN_PANEL_CLASS);
28
28
  this.model.name = this.context.localPath;
29
29
  this.model.unreadChanged.connect(this._unreadChanged);
30
+ // The context is ready once the document content has been loaded, which is
31
+ // when the chat can be given an ID and become usable.
32
+ this.context.ready
33
+ .then(() => this.model.markDocumentSynced())
34
+ .catch(e => console.error('The chat document failed to load', e));
30
35
  }
31
36
  /**
32
37
  * Dispose of the resources held by the widget.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab-chat",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "The library to build a chat based on shared document",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -42,21 +42,21 @@
42
42
  "watch:src": "tsc -w --sourceMap"
43
43
  },
44
44
  "dependencies": {
45
- "@jupyter/chat": "^0.23.0",
46
- "@jupyter/collaborative-drive": "^4.4.0 || ^5.0.0-alpha.0",
45
+ "@jupyter/chat": "^0.23.2",
46
+ "@jupyter/collaborative-drive": "^4.4.0 || ^5.0.0",
47
47
  "@jupyter/ydoc": "^3.0.0 || ^4.0.0",
48
- "@jupyterlab/application": "^4.6.0",
49
- "@jupyterlab/apputils": "^4.7.0",
50
- "@jupyterlab/coreutils": "^6.6.0",
51
- "@jupyterlab/docmanager": "^4.6.0",
52
- "@jupyterlab/docregistry": "^4.6.0",
53
- "@jupyterlab/launcher": "^4.6.0",
54
- "@jupyterlab/notebook": "^4.6.0",
55
- "@jupyterlab/rendermime": "^4.6.0",
56
- "@jupyterlab/services": "^7.6.0",
57
- "@jupyterlab/settingregistry": "^4.6.0",
58
- "@jupyterlab/translation": "^4.6.0",
59
- "@jupyterlab/ui-components": "^4.6.0",
48
+ "@jupyterlab/application": "^4.5.0",
49
+ "@jupyterlab/apputils": "^4.6.0",
50
+ "@jupyterlab/coreutils": "^6.5.0",
51
+ "@jupyterlab/docmanager": "^4.5.0",
52
+ "@jupyterlab/docregistry": "^4.5.0",
53
+ "@jupyterlab/launcher": "^4.5.0",
54
+ "@jupyterlab/notebook": "^4.5.0",
55
+ "@jupyterlab/rendermime": "^4.5.0",
56
+ "@jupyterlab/services": "^7.5.0",
57
+ "@jupyterlab/settingregistry": "^4.5.0",
58
+ "@jupyterlab/translation": "^4.5.0",
59
+ "@jupyterlab/ui-components": "^4.5.0",
60
60
  "@lumino/commands": "^2.3.3",
61
61
  "@lumino/coreutils": "^2.2.2",
62
62
  "@lumino/signaling": "^2.1.5",
@@ -66,7 +66,7 @@
66
66
  "yjs": "^13.5.40"
67
67
  },
68
68
  "devDependencies": {
69
- "@jupyterlab/testing": "^4.6.0",
69
+ "@jupyterlab/testing": "^4.5.0",
70
70
  "@types/jest": "^29.2.0",
71
71
  "@types/json-schema": "^7.0.11",
72
72
  "@types/react": "^18.2.0",
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { resolveChatRenamePath } from '../utils';
7
+
8
+ describe('resolveChatRenamePath', () => {
9
+ it('keeps a renamed chat in its original directory', () => {
10
+ expect(resolveChatRenamePath('chats/untitled.chat', 'test')).toBe(
11
+ 'chats/test.chat'
12
+ );
13
+ });
14
+
15
+ it('ensures the .chat extension', () => {
16
+ expect(resolveChatRenamePath('chats/untitled.chat', 'test.chat')).toBe(
17
+ 'chats/test.chat'
18
+ );
19
+ });
20
+
21
+ it('leaves root-level chats at the root', () => {
22
+ expect(resolveChatRenamePath('untitled.chat', 'test')).toBe('test.chat');
23
+ });
24
+
25
+ it('respects an explicit directory in the new name', () => {
26
+ expect(resolveChatRenamePath('chats/untitled.chat', 'archive/test')).toBe(
27
+ 'archive/test.chat'
28
+ );
29
+ });
30
+
31
+ it('preserves nested directories', () => {
32
+ expect(resolveChatRenamePath('chats/2024/untitled.chat', 'test')).toBe(
33
+ 'chats/2024/test.chat'
34
+ );
35
+ });
36
+ });
package/src/model.ts CHANGED
@@ -169,6 +169,28 @@ export class LabChatModel
169
169
  }
170
170
  }
171
171
 
172
+ /**
173
+ * Notify the model that its shared document has been synchronized with the
174
+ * server, and give it an ID if the document does not carry one yet.
175
+ *
176
+ * The model is only ready once it has an ID, so this must be called as soon as
177
+ * the document content is known - the ID cannot be created earlier, or a chat
178
+ * that already has one stored would end up with a conflicting ID.
179
+ *
180
+ * Callers should hook this to `IDocumentProvider.ready` (or the document
181
+ * context's `ready`) rather than to the document becoming clean: since
182
+ * jupyter-collaboration 5 a room stays dirty after being loaded, so the
183
+ * transition out of the dirty state may only happen on the first save, about a
184
+ * second later, leaving the chat unusable until then.
185
+ */
186
+ markDocumentSynced(): void {
187
+ if (!this._sharedModel.id) {
188
+ // Assigning the shared ID emits a metadata change, which sets the model
189
+ // ID - and therefore resolves `ready` - through `_onchange`.
190
+ this._sharedModel.id = UUID.uuid4();
191
+ }
192
+ }
193
+
172
194
  dispose(): void {
173
195
  if (this.isDisposed) {
174
196
  return;
@@ -563,6 +585,10 @@ export class LabChatModel
563
585
  }
564
586
 
565
587
  // Create a chat ID if not created when the document is not dirty.
588
+ //
589
+ // This is a fallback for chats whose document is not backed by a
590
+ // collaborative provider, and so never reports being synchronized. When
591
+ // there is one, `markDocumentSynced()` gets there first.
566
592
  if (changes.stateChange && !this._sharedModel.id) {
567
593
  if (
568
594
  changes.stateChange.some(
package/src/utils.ts CHANGED
@@ -31,3 +31,36 @@ export function getDisplayName(
31
31
  : '/' + path
32
32
  ).replace(pattern, '');
33
33
  }
34
+
35
+ /**
36
+ * Resolve the target path for a chat rename.
37
+ *
38
+ * When the new name is a bare basename (the usual case when renaming via the
39
+ * dialog), the chat must stay in the same directory as the original file.
40
+ * Otherwise `Contents.rename` treats the name as root-relative and moves the
41
+ * chat out of its directory (e.g. the configured `defaultDirectory`).
42
+ *
43
+ * @param oldPath - the current path of the chat file (e.g. `chats/untitled.chat`).
44
+ * @param newName - the new name or path entered by the user (e.g. `test`).
45
+ * @returns - the resolved path, with the `.chat` extension ensured and the
46
+ * original directory preserved when `newName` has no directory component.
47
+ */
48
+ export function resolveChatRenamePath(
49
+ oldPath: string,
50
+ newName: string
51
+ ): string {
52
+ let newPath = newName;
53
+
54
+ // Ensure the `.chat` extension.
55
+ if (!newPath.endsWith(chatFileType.extensions[0])) {
56
+ newPath = `${newPath}${chatFileType.extensions[0]}`;
57
+ }
58
+
59
+ // Bare basename (no separator) -> keep it in the original file's directory.
60
+ // An explicit directory in `newName` is respected as-is.
61
+ if (!newPath.includes('/')) {
62
+ newPath = PathExt.join(PathExt.dirname(oldPath), newPath);
63
+ }
64
+
65
+ return newPath;
66
+ }
package/src/widget.tsx CHANGED
@@ -23,6 +23,11 @@ export class LabChatPanel
23
23
  this.addClass(MAIN_PANEL_CLASS);
24
24
  this.model.name = this.context.localPath;
25
25
  this.model.unreadChanged.connect(this._unreadChanged);
26
+ // The context is ready once the document content has been loaded, which is
27
+ // when the chat can be given an ID and become usable.
28
+ this.context.ready
29
+ .then(() => this.model.markDocumentSynced())
30
+ .catch(e => console.error('The chat document failed to load', e));
26
31
  }
27
32
 
28
33
  /**