@jupyter/chat 0.23.0-alpha.2 → 0.23.0-alpha.4

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.
@@ -3,13 +3,16 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { ToolbarButton } from '@jupyterlab/ui-components';
6
+ import { ToolbarRegistry } from '@jupyterlab/apputils';
7
7
  import { RenderMimeRegistry } from '@jupyterlab/rendermime';
8
+ import { ObservableList } from '@jupyterlab/observables';
9
+ import { ToolbarButton, PanelWithToolbar } from '@jupyterlab/ui-components';
8
10
  import { Widget } from '@lumino/widgets';
9
11
 
10
12
  import { IChatPlaceholderFactory } from '../tokens';
11
13
  import { MultiChatPanel } from '../widgets/multichat-panel';
12
14
  import { defaultPlaceholder, Placeholder } from '../widgets/placeholder';
15
+ import { MockChatModel } from './mocks';
13
16
 
14
17
  describe('MultiChatPanel', () => {
15
18
  let rmRegistry: RenderMimeRegistry;
@@ -79,40 +82,6 @@ describe('MultiChatPanel', () => {
79
82
  });
80
83
  });
81
84
 
82
- describe('openInMain', () => {
83
- it('should not dispose the model when moving to main area', async () => {
84
- const { MockChatModel } = await import('./mocks');
85
- const model = new MockChatModel();
86
- const openInMain = jest.fn().mockResolvedValue(true);
87
-
88
- const panel = new MultiChatPanel({ rmRegistry, openInMain });
89
- panel.open({ model, displayName: 'test-chat' });
90
-
91
- // Find the 'moveMain' toolbar button by name and invoke its click handler directly.
92
- // ToolbarButton stores the onClick handler independently of DOM/React rendering,
93
- // so this works without attaching the widget to the document.
94
- const toolbar = panel.current!.toolbar;
95
- const names = Array.from(toolbar.names());
96
- const moveMainIdx = names.indexOf('moveMain');
97
- expect(moveMainIdx).toBeGreaterThan(-1);
98
-
99
- const moveButton = (toolbar.layout as any).widgets[
100
- moveMainIdx
101
- ] as ToolbarButton;
102
-
103
- moveButton.onClick();
104
- // Wait for the async onClick handler: openInMain resolves, then onClose is called.
105
- await Promise.resolve();
106
-
107
- expect(openInMain).toHaveBeenCalledWith(model.name);
108
- expect(model.isDisposed).toBe(false);
109
- expect(panel.getLoadedModel('test-chat')).toBeUndefined();
110
-
111
- model.dispose();
112
- panel.dispose();
113
- });
114
- });
115
-
116
85
  describe('placeholderFactory', () => {
117
86
  it('should use defaultPlaceholder when no factory is provided', () => {
118
87
  const panel = new MultiChatPanel({ rmRegistry });
@@ -207,7 +176,7 @@ describe('MultiChatPanel', () => {
207
176
  panel.dispose();
208
177
  });
209
178
 
210
- it('should call factory.create again when the placeholder is re-added after closing a chat', async () => {
179
+ it('should call factory.create again when the placeholder is re-added after closing a chat', () => {
211
180
  const factory: IChatPlaceholderFactory = {
212
181
  create: jest.fn().mockReturnValue(new Widget())
213
182
  };
@@ -219,7 +188,6 @@ describe('MultiChatPanel', () => {
219
188
  });
220
189
 
221
190
  // Simulate opening and closing a chat to trigger _addPlaceholder again.
222
- const { MockChatModel } = await import('./mocks');
223
191
  const model = new MockChatModel();
224
192
  panel.open({ model, displayName: 'test-chat' });
225
193
  panel.unsetLoadedModel('test-chat');
@@ -240,7 +208,95 @@ describe('MultiChatPanel', () => {
240
208
  '.jp-Toolbar-responsive-opener'
241
209
  );
242
210
  expect(opener).not.toBeNull();
211
+ panel.dispose();
212
+ });
213
+ });
214
+
215
+ describe('chatToolbarFactory', () => {
216
+ const makeFactory = (items: ToolbarRegistry.IToolbarItem[]) =>
217
+ jest.fn().mockReturnValue(new ObservableList({ values: items }));
218
+
219
+ it('should call factory with the panel when a chat is opened', () => {
220
+ const factory = makeFactory([{ name: 'myButton', widget: new Widget() }]);
221
+ const panel = new MultiChatPanel({
222
+ rmRegistry,
223
+ chatToolbarFactory: factory
224
+ });
225
+ const model = new MockChatModel();
226
+ const chatWidget = panel.open({ model, displayName: 'test' });
227
+ expect(factory).toHaveBeenCalledTimes(1);
228
+ expect(factory.mock.calls[0][0].widget).toBe(chatWidget);
229
+ panel.dispose();
230
+ });
243
231
 
232
+ it('should add the created widget to the chat toolbar', () => {
233
+ const panel = new MultiChatPanel({
234
+ rmRegistry,
235
+ chatToolbarFactory: makeFactory([
236
+ { name: 'myButton', widget: new Widget() }
237
+ ])
238
+ });
239
+ const model = new MockChatModel();
240
+ panel.open({ model, displayName: 'test' });
241
+ const toolbar = (panel.current as unknown as PanelWithToolbar).toolbar;
242
+ expect(Array.from(toolbar.names())).toContain('myButton');
243
+ panel.dispose();
244
+ });
245
+
246
+ it('should insert the item before "close"', () => {
247
+ const panel = new MultiChatPanel({
248
+ rmRegistry,
249
+ chatToolbarFactory: makeFactory([
250
+ { name: 'myButton', widget: new Widget() }
251
+ ])
252
+ });
253
+ const model = new MockChatModel();
254
+ panel.open({ model, displayName: 'test' });
255
+ const toolbar = (panel.current as unknown as PanelWithToolbar).toolbar;
256
+ const names = Array.from(toolbar.names());
257
+ expect(names.indexOf('myButton')).toBeLessThan(names.indexOf('close'));
258
+ panel.dispose();
259
+ });
260
+
261
+ it('should add all items when factory returns multiple items', () => {
262
+ const panel = new MultiChatPanel({
263
+ rmRegistry,
264
+ chatToolbarFactory: makeFactory([
265
+ { name: 'item1', widget: new Widget() },
266
+ { name: 'item2', widget: new Widget() }
267
+ ])
268
+ });
269
+ const model = new MockChatModel();
270
+ panel.open({ model, displayName: 'test' });
271
+ const toolbar = (panel.current as unknown as PanelWithToolbar).toolbar;
272
+ const names = Array.from(toolbar.names());
273
+ expect(names).toContain('item1');
274
+ expect(names).toContain('item2');
275
+ panel.dispose();
276
+ });
277
+
278
+ it('should call factory again for each newly opened chat', () => {
279
+ const factory = jest.fn().mockImplementation(
280
+ () =>
281
+ new ObservableList<ToolbarRegistry.IToolbarItem>({
282
+ values: [{ name: 'myButton', widget: new Widget() }]
283
+ })
284
+ );
285
+ const panel = new MultiChatPanel({
286
+ rmRegistry,
287
+ chatToolbarFactory: factory
288
+ });
289
+ const model1 = new MockChatModel();
290
+ panel.open({ model: model1, displayName: 'chat1' });
291
+ panel.unsetLoadedModel('chat1');
292
+
293
+ const model2 = new MockChatModel();
294
+ panel.open({ model: model2, displayName: 'chat2' });
295
+
296
+ expect(factory).toHaveBeenCalledTimes(2);
297
+ expect(factory.mock.results[0].value).not.toBe(
298
+ factory.mock.results[1].value
299
+ );
244
300
  panel.dispose();
245
301
  });
246
302
  });
@@ -10,8 +10,8 @@ import { MessageRenderer } from './message-renderer';
10
10
  import { AttachmentPreviewList } from '../attachments';
11
11
  import { ChatInput } from '../input';
12
12
  import { useChatContext } from '../../context';
13
- import { IInputModel, InputModel } from '../../input-model';
14
- import { IMessageContent, IMessage } from '../../types';
13
+ import { InputModel } from '../../input-model';
14
+ import { IMessageContent, IMessage, INewMessage } from '../../types';
15
15
  import { replaceSpanToMention } from '../../utils';
16
16
 
17
17
  export const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
@@ -96,8 +96,8 @@ export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
96
96
  });
97
97
  const inputModel = new InputModel({
98
98
  chatContext: model.createChatContext(),
99
- onSend: (input: string, model?: IInputModel) =>
100
- updateMessage(message.id, input, model),
99
+ onSend: (newMessage: INewMessage) =>
100
+ updateMessage(message.id, newMessage),
101
101
  onCancel: () => cancelEdition(),
102
102
  value: body,
103
103
  activeCellManager: model.activeCellManager,
@@ -121,17 +121,27 @@ export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
121
121
 
122
122
  // Update the content of the message.
123
123
  const updateMessage = useCallback(
124
- (id: string, input: string, inputModel?: IInputModel): void => {
125
- if (!canEdit || !inputModel) {
124
+ (id: string, newMessage: INewMessage): void => {
125
+ const inputModel = model.getEditionModel(id);
126
+ if (!canEdit || !inputModel || !model.updateMessage) {
127
+ setEdit(false);
126
128
  return;
127
129
  }
130
+
128
131
  // Update the message
129
132
  const updatedMessage = { ...message };
130
- updatedMessage.body = input;
131
- updatedMessage.attachments = inputModel.attachments;
132
- updatedMessage.mentions = inputModel.mentions;
133
- model.updateMessage!(id, updatedMessage);
134
- model.getEditionModel(message.id)?.dispose();
133
+ if (newMessage.body) {
134
+ updatedMessage.body = newMessage.body;
135
+ }
136
+ if (newMessage.attachments) {
137
+ updatedMessage.attachments = newMessage.attachments;
138
+ }
139
+ if (newMessage.mentions) {
140
+ updatedMessage.mentions = newMessage.mentions;
141
+ }
142
+
143
+ model.updateMessage(id, updatedMessage);
144
+ inputModel.dispose();
135
145
  setEdit(false);
136
146
  },
137
147
  [model, message, canEdit]
package/src/index.ts CHANGED
@@ -13,4 +13,5 @@ export * from './registers';
13
13
  export * from './selection-watcher';
14
14
  export * from './tokens';
15
15
  export * from './types';
16
+ export * from './utils';
16
17
  export * from './widgets';
@@ -10,7 +10,13 @@ import { ISignal, Signal } from '@lumino/signaling';
10
10
  import { IActiveCellManager } from './active-cell-manager';
11
11
  import { ISelectionWatcher } from './selection-watcher';
12
12
  import { IChatContext } from './model';
13
- import { IAttachment, INotebookAttachment, IUser } from './types';
13
+ import {
14
+ IAttachment,
15
+ IMessageMetadata,
16
+ INewMessage,
17
+ INotebookAttachment,
18
+ IUser
19
+ } from './types';
14
20
 
15
21
  /**
16
22
  * The chat input interface.
@@ -152,6 +158,31 @@ export interface IInputModel extends IDisposable {
152
158
  */
153
159
  clearMentions(): void;
154
160
 
161
+ /**
162
+ * Get the metadata to attach to the next message to send.
163
+ */
164
+ getMetadata(): IMessageMetadata;
165
+
166
+ /**
167
+ * Merge a patch into the metadata to attach to the next message to send.
168
+ *
169
+ * This is a *shallow* merge: each key in `patch` replaces the existing value
170
+ * at that key wholesale, rather than being merged recursively. To change a
171
+ * nested field, pass the whole new value for its top-level key. The patch is
172
+ * deep-copied, so mutating it afterwards won't affect the stored metadata.
173
+ */
174
+ updateMetadata(patch: IMessageMetadata): void;
175
+
176
+ /**
177
+ * Clear the metadata.
178
+ */
179
+ clearMetadata(): void;
180
+
181
+ /**
182
+ * A signal emitting when the metadata has changed.
183
+ */
184
+ readonly metadataChanged?: ISignal<IInputModel, IMessageMetadata>;
185
+
155
186
  /**
156
187
  * A signal emitting when disposing of the model.
157
188
  */
@@ -169,6 +200,7 @@ export class InputModel implements IInputModel {
169
200
  this._value = options.value || '';
170
201
  this._attachments = options.attachments || [];
171
202
  this._mentions = options.mentions || [];
203
+ this._metadata = options.metadata || {};
172
204
  this.cursorIndex = options.cursorIndex || this.value.length;
173
205
  this._activeCellManager = options.activeCellManager ?? null;
174
206
  this._selectionWatcher = options.selectionWatcher ?? null;
@@ -194,8 +226,32 @@ export class InputModel implements IInputModel {
194
226
  * Function to send a message.
195
227
  */
196
228
  send = (input: string): void => {
197
- this._onSend(input, this);
229
+ const message: INewMessage = {
230
+ body: input
231
+ };
232
+
233
+ // Add the attachments
234
+ if (this.attachments.length) {
235
+ message.attachments = [...this.attachments];
236
+ }
237
+
238
+ // Add the mentions
239
+ if (this.mentions.length) {
240
+ message.mentions = [...this.mentions];
241
+ }
242
+
243
+ // Add the metadata
244
+ if (Object.keys(this._metadata).length) {
245
+ message.metadata = { ...this._metadata };
246
+ }
247
+
248
+ // Send the message
249
+ this._onSend(message);
250
+
251
+ // Clear the input
198
252
  this.value = '';
253
+ this.clearAttachments();
254
+ this.clearMentions();
199
255
  };
200
256
 
201
257
  /**
@@ -460,6 +516,45 @@ export class InputModel implements IInputModel {
460
516
  this._mentions = [];
461
517
  };
462
518
 
519
+ /**
520
+ * Get the metadata to attach to the next message to send.
521
+ */
522
+ getMetadata(): IMessageMetadata {
523
+ return this._metadata;
524
+ }
525
+
526
+ /**
527
+ * Merge a patch into the metadata to attach to the next message to send.
528
+ *
529
+ * This is a *shallow* merge: each key in `patch` replaces the existing value
530
+ * at that key wholesale, rather than being merged recursively. To change a
531
+ * nested field, pass the whole new value for its top-level key. The patch is
532
+ * deep-copied, so mutating it afterwards won't affect the stored metadata.
533
+ */
534
+ updateMetadata = (patch: IMessageMetadata): void => {
535
+ // Deep-copy the patch so a caller mutating a nested field afterwards can't
536
+ // reach into the stored metadata. `this._metadata` only ever holds values
537
+ // that were themselves deep-copied on the way in, so a shallow spread of it
538
+ // is safe.
539
+ this._metadata = { ...this._metadata, ...structuredClone(patch) };
540
+ this._metadataChanged.emit(this._metadata);
541
+ };
542
+
543
+ /**
544
+ * Clear the metadata.
545
+ */
546
+ clearMetadata = (): void => {
547
+ this._metadata = {};
548
+ this._metadataChanged.emit(this._metadata);
549
+ };
550
+
551
+ /**
552
+ * A signal emitting when the metadata has changed.
553
+ */
554
+ get metadataChanged(): ISignal<IInputModel, IMessageMetadata> {
555
+ return this._metadataChanged;
556
+ }
557
+
463
558
  /**
464
559
  * Dispose the input model.
465
560
  */
@@ -486,13 +581,14 @@ export class InputModel implements IInputModel {
486
581
  }
487
582
 
488
583
  private _id: string;
489
- private _onSend: (input: string, model?: InputModel) => void;
584
+ private _onSend: (message: INewMessage) => void;
490
585
  private _chatContext?: IChatContext;
491
586
  private _value: string;
492
587
  private _cursorIndex: number | null = null;
493
588
  private _currentWord: string | null = null;
494
589
  private _attachments: IAttachment[];
495
590
  private _mentions: IUser[];
591
+ private _metadata: IMessageMetadata;
496
592
  private _activeCellManager: IActiveCellManager | null;
497
593
  private _selectionWatcher: ISelectionWatcher | null;
498
594
  private _documentManager: IDocumentManager | null;
@@ -503,6 +599,7 @@ export class InputModel implements IInputModel {
503
599
  private _configChanged = new Signal<IInputModel, InputModel.IConfig>(this);
504
600
  private _focusInputSignal = new Signal<InputModel, void>(this);
505
601
  private _attachmentsChanged = new Signal<InputModel, IAttachment[]>(this);
602
+ private _metadataChanged = new Signal<InputModel, IMessageMetadata>(this);
506
603
  private _onDisposed = new Signal<InputModel, void>(this);
507
604
  private _isDisposed = false;
508
605
  }
@@ -519,7 +616,7 @@ export namespace InputModel {
519
616
  * @param content - the content of the message.
520
617
  * @param model - the model of the input sending the message.
521
618
  */
522
- onSend: (content: string, model?: InputModel) => void;
619
+ onSend: (message: INewMessage) => void;
523
620
 
524
621
  /**
525
622
  * Function that should cancel the message edition.
@@ -541,6 +638,11 @@ export namespace InputModel {
541
638
  */
542
639
  mentions?: IUser[];
543
640
 
641
+ /**
642
+ * The initial metadata.
643
+ */
644
+ metadata?: IMessageMetadata;
645
+
544
646
  /**
545
647
  * The current cursor index.
546
648
  * This refers to the index of the character in front of the cursor.
package/src/model.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  nullTranslator,
10
10
  TranslationBundle
11
11
  } from '@jupyterlab/translation';
12
+ import type { IAwareness } from '@jupyter/ydoc';
12
13
  import { ArrayExt } from '@lumino/algorithm';
13
14
  import { CommandRegistry } from '@lumino/commands';
14
15
  import { PromiseDelegate } from '@lumino/coreutils';
@@ -53,6 +54,14 @@ export interface IChatModel extends IDisposable {
53
54
  */
54
55
  readonly ready: Promise<void>;
55
56
 
57
+ /**
58
+ * The awareness channel of the underlying shared document, when the chat is
59
+ * backed by one. Lets extensions read collaborative state attached to the
60
+ * chat (e.g. session information published by AI personas) through a
61
+ * supported API instead of reaching into a concrete implementation.
62
+ */
63
+ readonly awareness?: IAwareness;
64
+
56
65
  /**
57
66
  * The indexes list of the messages currently in the viewport.
58
67
  */
@@ -264,7 +273,7 @@ export abstract class AbstractChatModel implements IChatModel {
264
273
  config: {
265
274
  sendWithShiftEnter: config.sendWithShiftEnter
266
275
  },
267
- onSend: (input: string) => this.sendMessage({ body: input })
276
+ onSend: this.sendMessage.bind(this)
268
277
  });
269
278
 
270
279
  this._commands = options.commands;
@@ -894,6 +903,11 @@ export interface IChatContext {
894
903
  * Current user connected with the chat panel
895
904
  */
896
905
  readonly user: IUser | undefined;
906
+ /**
907
+ * The awareness channel of the underlying shared document, when the chat is
908
+ * backed by one.
909
+ */
910
+ readonly awareness?: IAwareness;
897
911
  }
898
912
 
899
913
  /**
@@ -917,6 +931,10 @@ export abstract class AbstractChatContext implements IChatContext {
917
931
  return this._model?.user;
918
932
  }
919
933
 
934
+ get awareness(): IAwareness | undefined {
935
+ return this._model.awareness;
936
+ }
937
+
920
938
  /**
921
939
  * ABSTRACT: Should return a list of users who have connected to this chat.
922
940
  */
package/src/tokens.ts CHANGED
@@ -3,24 +3,40 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
6
+ import { IWidgetTracker } from '@jupyterlab/apputils';
7
7
  import { Token } from '@lumino/coreutils';
8
8
  import { Widget } from '@lumino/widgets';
9
9
 
10
10
  import { ChatWidget, Placeholder } from './widgets';
11
11
  import { IChatModel } from './model';
12
+ import { ChatArea } from './types';
12
13
 
13
14
  /**
14
- * The main area chat widget type.
15
+ * The interface for any widget displaying a chat (main area or side panel).
15
16
  */
16
- export type MainAreaChat = MainAreaWidget<ChatWidget> & {
17
+ export interface IChatPanel extends Widget {
18
+ /**
19
+ * The chat widget embedded in the panel.
20
+ */
21
+ widget: ChatWidget;
22
+ /**
23
+ * The model of the chat widget.
24
+ */
17
25
  model: IChatModel;
18
- };
26
+ /**
27
+ * The area of the panel.
28
+ */
29
+ area: ChatArea;
30
+ /**
31
+ * The chat panel toolbar.
32
+ */
33
+ toolbar: Widget;
34
+ }
19
35
 
20
36
  /**
21
37
  * the chat tracker type.
22
38
  */
23
- export type IChatTracker = IWidgetTracker<ChatWidget | MainAreaChat>;
39
+ export type IChatTracker = IWidgetTracker<IChatPanel>;
24
40
 
25
41
  /**
26
42
  * A chat tracker token.
package/src/types.ts CHANGED
@@ -174,10 +174,12 @@ export interface IChatHistory {
174
174
  /**
175
175
  * The content of a new message.
176
176
  */
177
- export interface INewMessage {
178
- body: string;
179
- id?: string;
180
- }
177
+ export type INewMessage<T = IUser, U = IAttachment> = Partial<
178
+ Pick<
179
+ IMessageContent<T, U>,
180
+ 'body' | 'attachments' | 'mentions' | 'metadata' | 'mime_model' | 'sender'
181
+ >
182
+ >;
181
183
 
182
184
  /**
183
185
  * The attachment type. Jupyter Chat allows for two types of attachments
package/src/utils.ts CHANGED
@@ -3,13 +3,17 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import { CommandToolbarButton, ToolbarRegistry } from '@jupyterlab/apputils';
6
7
  import { CodeEditor } from '@jupyterlab/codeeditor';
7
8
  import { CodeMirrorEditor } from '@jupyterlab/codemirror';
8
9
  import { DocumentWidget } from '@jupyterlab/docregistry';
9
10
  import { FileEditor } from '@jupyterlab/fileeditor';
10
11
  import { Notebook } from '@jupyterlab/notebook';
12
+ import { IObservableList, ObservableList } from '@jupyterlab/observables';
13
+ import { CommandRegistry } from '@lumino/commands';
11
14
  import { Widget } from '@lumino/widgets';
12
15
 
16
+ import { IChatPanel } from './tokens';
13
17
  import { IUser } from './types';
14
18
 
15
19
  const MENTION_CLASS = 'jp-chat-mention';
@@ -81,3 +85,58 @@ export function replaceSpanToMention(content: string, user: IUser): string {
81
85
  const regex = new RegExp(mentionEl, 'g');
82
86
  return content.replace(regex, mention);
83
87
  }
88
+
89
+ /**
90
+ * Wraps a toolbar factory so that every CommandToolbarButton it creates
91
+ * automatically receives the panel's area as an arg.
92
+ * This lets commands branch on `args.area` without each plugin having to
93
+ * register a per-item toolbarRegistry.addFactory call.
94
+ */
95
+ export function injectAreaArg(
96
+ baseFactory: (
97
+ panel: IChatPanel
98
+ ) => IObservableList<ToolbarRegistry.IToolbarItem>,
99
+ commands: CommandRegistry
100
+ ): (panel: IChatPanel) => IObservableList<ToolbarRegistry.IToolbarItem> {
101
+ return (panel: IChatPanel) => {
102
+ const base = baseFactory(panel);
103
+
104
+ const inject = (
105
+ item: ToolbarRegistry.IToolbarItem
106
+ ): ToolbarRegistry.IToolbarItem => {
107
+ const { widget } = item;
108
+ if (!(widget instanceof CommandToolbarButton)) {
109
+ return item;
110
+ }
111
+ return {
112
+ name: item.name,
113
+ widget: new CommandToolbarButton({
114
+ commands,
115
+ id: widget.commandId,
116
+ args: { area: panel.area }
117
+ })
118
+ };
119
+ };
120
+
121
+ const wrapped = new ObservableList<ToolbarRegistry.IToolbarItem>({
122
+ values: Array.from({ length: base.length }, (_, i) => inject(base.get(i)))
123
+ });
124
+
125
+ base.changed.connect((_, change) => {
126
+ if (change.type === 'add') {
127
+ wrapped.insertAll(change.newIndex, change.newValues.map(inject));
128
+ } else if (change.type === 'remove') {
129
+ wrapped.removeRange(
130
+ change.oldIndex,
131
+ change.oldIndex + change.oldValues.length
132
+ );
133
+ } else if (change.type === 'set') {
134
+ change.newValues.forEach((item, i) =>
135
+ wrapped.set(change.newIndex + i, inject(item))
136
+ );
137
+ }
138
+ });
139
+
140
+ return wrapped;
141
+ };
142
+ }