@jupyter/chat 0.4.0 → 0.6.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 (45) hide show
  1. package/lib/active-cell-manager.d.ts +3 -0
  2. package/lib/components/chat-input.d.ts +4 -0
  3. package/lib/components/chat-input.js +32 -15
  4. package/lib/components/chat-messages.d.ts +31 -1
  5. package/lib/components/chat-messages.js +57 -19
  6. package/lib/components/chat.js +1 -1
  7. package/lib/components/code-blocks/code-toolbar.js +51 -17
  8. package/lib/components/input/cancel-button.d.ts +12 -0
  9. package/lib/components/input/cancel-button.js +27 -0
  10. package/lib/components/input/send-button.d.ts +18 -0
  11. package/lib/components/input/send-button.js +143 -0
  12. package/lib/components/mui-extras/tooltipped-button.d.ts +41 -0
  13. package/lib/components/mui-extras/tooltipped-button.js +43 -0
  14. package/lib/components/mui-extras/tooltipped-icon-button.js +5 -1
  15. package/lib/components/rendermime-markdown.js +15 -6
  16. package/lib/icons.d.ts +1 -0
  17. package/lib/icons.js +5 -0
  18. package/lib/index.d.ts +2 -1
  19. package/lib/index.js +2 -1
  20. package/lib/model.d.ts +51 -8
  21. package/lib/model.js +44 -12
  22. package/lib/selection-watcher.d.ts +62 -0
  23. package/lib/selection-watcher.js +134 -0
  24. package/lib/types.d.ts +22 -0
  25. package/lib/utils.d.ts +11 -0
  26. package/lib/utils.js +37 -0
  27. package/package.json +2 -1
  28. package/src/active-cell-manager.ts +3 -0
  29. package/src/components/chat-input.tsx +48 -30
  30. package/src/components/chat-messages.tsx +112 -32
  31. package/src/components/chat.tsx +1 -1
  32. package/src/components/code-blocks/code-toolbar.tsx +56 -18
  33. package/src/components/input/cancel-button.tsx +47 -0
  34. package/src/components/input/send-button.tsx +210 -0
  35. package/src/components/mui-extras/tooltipped-button.tsx +92 -0
  36. package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -1
  37. package/src/components/rendermime-markdown.tsx +16 -5
  38. package/src/icons.ts +6 -0
  39. package/src/index.ts +2 -1
  40. package/src/model.ts +77 -13
  41. package/src/selection-watcher.ts +221 -0
  42. package/src/types.ts +25 -0
  43. package/src/utils.ts +47 -0
  44. package/style/chat.css +13 -0
  45. package/style/icons/include-selection.svg +5 -0
@@ -0,0 +1,210 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown';
7
+ import SendIcon from '@mui/icons-material/Send';
8
+ import { Box, Menu, MenuItem, Typography } from '@mui/material';
9
+ import React, { useCallback, useEffect, useState } from 'react';
10
+
11
+ import { IChatModel } from '../../model';
12
+ import { TooltippedButton } from '../mui-extras/tooltipped-button';
13
+ import { includeSelectionIcon } from '../../icons';
14
+ import { Selection } from '../../types';
15
+
16
+ const SEND_BUTTON_CLASS = 'jp-chat-send-button';
17
+ const SEND_INCLUDE_OPENER_CLASS = 'jp-chat-send-include-opener';
18
+ const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
19
+
20
+ /**
21
+ * The send button props.
22
+ */
23
+ export type SendButtonProps = {
24
+ model: IChatModel;
25
+ sendWithShiftEnter: boolean;
26
+ inputExists: boolean;
27
+ onSend: (selection?: Selection) => unknown;
28
+ hideIncludeSelection?: boolean;
29
+ hasButtonOnLeft?: boolean;
30
+ };
31
+
32
+ /**
33
+ * The send button, with optional 'include selection' menu.
34
+ */
35
+ export function SendButton(props: SendButtonProps): JSX.Element {
36
+ const { activeCellManager, selectionWatcher } = props.model;
37
+ const hideIncludeSelection = props.hideIncludeSelection ?? false;
38
+ const hasButtonOnLeft = props.hasButtonOnLeft ?? false;
39
+ const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
40
+ const [menuOpen, setMenuOpen] = useState(false);
41
+
42
+ const openMenu = useCallback((el: HTMLElement | null) => {
43
+ setMenuAnchorEl(el);
44
+ setMenuOpen(true);
45
+ }, []);
46
+
47
+ const closeMenu = useCallback(() => {
48
+ setMenuOpen(false);
49
+ }, []);
50
+
51
+ const disabled = !props.inputExists;
52
+
53
+ const [selectionTooltip, setSelectionTooltip] = useState<string>('');
54
+ const [disableInclude, setDisableInclude] = useState<boolean>(true);
55
+
56
+ useEffect(() => {
57
+ /**
58
+ * Enable or disable the include selection button, and adapt the tooltip.
59
+ */
60
+ const toggleIncludeState = () => {
61
+ setDisableInclude(
62
+ !(selectionWatcher?.selection || activeCellManager?.available)
63
+ );
64
+ const tooltip = selectionWatcher?.selection
65
+ ? `${selectionWatcher.selection.numLines} line(s) selected`
66
+ : activeCellManager?.available
67
+ ? 'Code from 1 active cell'
68
+ : 'No selection or active cell';
69
+ setSelectionTooltip(tooltip);
70
+ };
71
+
72
+ if (!hideIncludeSelection) {
73
+ selectionWatcher?.selectionChanged.connect(toggleIncludeState);
74
+ activeCellManager?.availabilityChanged.connect(toggleIncludeState);
75
+ toggleIncludeState();
76
+ }
77
+ return () => {
78
+ selectionWatcher?.selectionChanged.disconnect(toggleIncludeState);
79
+ activeCellManager?.availabilityChanged.disconnect(toggleIncludeState);
80
+ };
81
+ }, [activeCellManager, selectionWatcher, hideIncludeSelection]);
82
+
83
+ const defaultTooltip = props.sendWithShiftEnter
84
+ ? 'Send message (SHIFT+ENTER)'
85
+ : 'Send message (ENTER)';
86
+ const tooltip = defaultTooltip;
87
+
88
+ function sendWithSelection() {
89
+ // Append the selected text if exists.
90
+ if (selectionWatcher?.selection) {
91
+ props.onSend({
92
+ type: 'text',
93
+ source: selectionWatcher.selection.text
94
+ });
95
+ closeMenu();
96
+ return;
97
+ }
98
+
99
+ // Append the active cell content if exists.
100
+ if (activeCellManager?.available) {
101
+ props.onSend({
102
+ type: 'cell',
103
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
104
+ source: activeCellManager.getContent(false)!.source
105
+ });
106
+ closeMenu();
107
+ return;
108
+ }
109
+ }
110
+
111
+ return (
112
+ <Box sx={{ display: 'flex', flexWrap: 'nowrap' }}>
113
+ <TooltippedButton
114
+ onClick={() => props.onSend()}
115
+ disabled={disabled}
116
+ tooltip={tooltip}
117
+ buttonProps={{
118
+ size: 'small',
119
+ title: defaultTooltip,
120
+ variant: 'contained',
121
+ className: SEND_BUTTON_CLASS
122
+ }}
123
+ sx={{
124
+ minWidth: 'unset',
125
+ borderTopLeftRadius: hasButtonOnLeft ? '0px' : '2px',
126
+ borderTopRightRadius: hideIncludeSelection ? '2px' : '0px',
127
+ borderBottomRightRadius: hideIncludeSelection ? '2px' : '0px',
128
+ borderBottomLeftRadius: hasButtonOnLeft ? '0px' : '2px'
129
+ }}
130
+ >
131
+ <SendIcon />
132
+ </TooltippedButton>
133
+ {!hideIncludeSelection && (
134
+ <>
135
+ <TooltippedButton
136
+ onClick={e => {
137
+ openMenu(e.currentTarget);
138
+ }}
139
+ disabled={disabled}
140
+ tooltip=""
141
+ buttonProps={{
142
+ variant: 'contained',
143
+ onKeyDown: e => {
144
+ if (e.key !== 'Enter' && e.key !== ' ') {
145
+ return;
146
+ }
147
+ openMenu(e.currentTarget);
148
+ // stopping propagation of this event prevents the prompt from being
149
+ // sent when the dropdown button is selected and clicked via 'Enter'.
150
+ e.stopPropagation();
151
+ },
152
+ className: SEND_INCLUDE_OPENER_CLASS
153
+ }}
154
+ sx={{
155
+ minWidth: 'unset',
156
+ padding: '4px 0px',
157
+ borderRadius: '0px 2px 2px 0px',
158
+ marginLeft: '1px'
159
+ }}
160
+ >
161
+ <KeyboardArrowDown />
162
+ </TooltippedButton>
163
+ <Menu
164
+ open={menuOpen}
165
+ onClose={closeMenu}
166
+ anchorEl={menuAnchorEl}
167
+ anchorOrigin={{
168
+ vertical: 'top',
169
+ horizontal: 'right'
170
+ }}
171
+ transformOrigin={{
172
+ vertical: 'bottom',
173
+ horizontal: 'right'
174
+ }}
175
+ sx={{
176
+ '& .MuiMenuItem-root': {
177
+ display: 'flex',
178
+ alignItems: 'center',
179
+ gap: '8px'
180
+ },
181
+ '& svg': {
182
+ lineHeight: 0
183
+ }
184
+ }}
185
+ >
186
+ <MenuItem
187
+ onClick={e => {
188
+ sendWithSelection();
189
+ // prevent sending second message with no selection
190
+ e.stopPropagation();
191
+ }}
192
+ disabled={disableInclude}
193
+ className={SEND_INCLUDE_LI_CLASS}
194
+ >
195
+ <includeSelectionIcon.react />
196
+ <Box>
197
+ <Typography display="block">
198
+ Send message with selection
199
+ </Typography>
200
+ <Typography display="block" sx={{ opacity: 0.618 }}>
201
+ {selectionTooltip}
202
+ </Typography>
203
+ </Box>
204
+ </MenuItem>
205
+ </Menu>
206
+ </>
207
+ )}
208
+ </Box>
209
+ );
210
+ }
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React from 'react';
7
+ import { Button, ButtonProps, SxProps, TooltipProps } from '@mui/material';
8
+
9
+ import { ContrastingTooltip } from './contrasting-tooltip';
10
+
11
+ export type TooltippedButtonProps = {
12
+ onClick: React.MouseEventHandler<HTMLButtonElement>;
13
+ tooltip: string;
14
+ children: JSX.Element;
15
+ disabled?: boolean;
16
+ placement?: TooltipProps['placement'];
17
+ /**
18
+ * The offset of the tooltip popup.
19
+ *
20
+ * The expected syntax is defined by the Popper library:
21
+ * https://popper.js.org/docs/v2/modifiers/offset/
22
+ */
23
+ offset?: [number, number];
24
+ 'aria-label'?: string;
25
+ /**
26
+ * Props passed directly to the MUI `Button` component.
27
+ */
28
+ buttonProps?: ButtonProps;
29
+ /**
30
+ * Styles applied to the MUI `Button` component.
31
+ */
32
+ sx?: SxProps;
33
+ };
34
+
35
+ /**
36
+ * A component that renders an MUI `Button` with a high-contrast tooltip
37
+ * provided by `ContrastingTooltip`. This component differs from the MUI
38
+ * defaults in the following ways:
39
+ *
40
+ * - Shows the tooltip on hover even if disabled.
41
+ * - Renders the tooltip above the button by default.
42
+ * - Renders the tooltip closer to the button by default.
43
+ * - Lowers the opacity of the Button when disabled.
44
+ * - Renders the Button with `line-height: 0` to avoid showing extra
45
+ * vertical space in SVG icons.
46
+ *
47
+ * NOTE TO DEVS: Please keep this component's features synchronized with
48
+ * features available to `TooltippedIconButton`.
49
+ */
50
+ export function TooltippedButton(props: TooltippedButtonProps): JSX.Element {
51
+ return (
52
+ <ContrastingTooltip
53
+ title={props.tooltip}
54
+ placement={props.placement ?? 'top'}
55
+ slotProps={{
56
+ popper: {
57
+ modifiers: [
58
+ {
59
+ name: 'offset',
60
+ options: {
61
+ offset: [0, -8]
62
+ }
63
+ }
64
+ ]
65
+ }
66
+ }}
67
+ >
68
+ {/*
69
+ By default, tooltips never appear when the Button is disabled. The
70
+ official way to support this feature in MUI is to wrap the child Button
71
+ element in a `span` element.
72
+
73
+ See: https://mui.com/material-ui/react-tooltip/#disabled-elements
74
+ */}
75
+ <span style={{ cursor: 'default' }}>
76
+ <Button
77
+ {...props.buttonProps}
78
+ onClick={props.onClick}
79
+ disabled={props.disabled}
80
+ sx={{
81
+ lineHeight: 0,
82
+ ...(props.disabled && { opacity: 0.5 }),
83
+ ...props.sx
84
+ }}
85
+ aria-label={props['aria-label']}
86
+ >
87
+ {props.children}
88
+ </Button>
89
+ </span>
90
+ </ContrastingTooltip>
91
+ );
92
+ }
@@ -73,7 +73,11 @@ export function TooltippedIconButton(
73
73
  {...props.iconButtonProps}
74
74
  onClick={props.onClick}
75
75
  disabled={props.disabled}
76
- sx={{ lineHeight: 0, ...(props.disabled && { opacity: 0.5 }) }}
76
+ sx={{
77
+ marginLeft: '8px',
78
+ lineHeight: 0,
79
+ ...(props.disabled && { opacity: 0.5 })
80
+ }}
77
81
  aria-label={props['aria-label']}
78
82
  >
79
83
  {props.children}
@@ -24,14 +24,19 @@ type RendermimeMarkdownProps = {
24
24
  };
25
25
 
26
26
  /**
27
- * Takes \( and returns \\(. Escapes LaTeX delimeters by adding extra backslashes where needed for proper rendering by @jupyterlab/rendermime.
27
+ * Escapes backslashes in LaTeX delimiters such that they appear in the DOM
28
+ * after the initial MarkDown render. For example, this function takes '\(` and
29
+ * returns `\\(`.
30
+ *
31
+ * Required for proper rendering of MarkDown + LaTeX markup in the chat by
32
+ * `ILatexTypesetter`.
28
33
  */
29
34
  function escapeLatexDelimiters(text: string) {
30
35
  return text
31
- .replace('\\(', '\\\\(')
32
- .replace('\\)', '\\\\)')
33
- .replace('\\[', '\\\\[')
34
- .replace('\\]', '\\\\]');
36
+ .replace('\\(/g', '\\\\(')
37
+ .replace('\\)/g', '\\\\)')
38
+ .replace('\\[/g', '\\\\[')
39
+ .replace('\\]/g', '\\\\]');
35
40
  }
36
41
 
37
42
  function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
@@ -47,12 +52,15 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
47
52
 
48
53
  useEffect(() => {
49
54
  const renderContent = async () => {
55
+ // initialize mime model
50
56
  const mdStr = escapeLatexDelimiters(props.markdownStr);
51
57
  const model = props.rmRegistry.createModel({
52
58
  data: { [MD_MIME_TYPE]: mdStr }
53
59
  });
54
60
 
55
61
  const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
62
+
63
+ // step 1: render markdown
56
64
  await renderer.renderModel(model);
57
65
  props.rmRegistry.latexTypesetter?.typeset(renderer.node);
58
66
  if (!renderer.node) {
@@ -61,6 +69,9 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
61
69
  );
62
70
  }
63
71
 
72
+ // step 2: render LaTeX via MathJax.
73
+ props.rmRegistry.latexTypesetter?.typeset(renderer.node);
74
+
64
75
  const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
65
76
 
66
77
  // Attach CodeToolbar root element to each <pre> block
package/src/icons.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { LabIcon } from '@jupyterlab/ui-components';
9
9
 
10
10
  import chatSvgStr from '../style/icons/chat.svg';
11
+ import includeSelectionIconStr from '../style/icons/include-selection.svg';
11
12
  import readSvgStr from '../style/icons/read.svg';
12
13
  import replaceCellSvg from '../style/icons/replace-cell.svg';
13
14
 
@@ -25,3 +26,8 @@ export const replaceCellIcon = new LabIcon({
25
26
  name: 'jupyter-ai::replace-cell',
26
27
  svgstr: replaceCellSvg
27
28
  });
29
+
30
+ export const includeSelectionIcon = new LabIcon({
31
+ name: 'jupyter-chat::include',
32
+ svgstr: includeSelectionIconStr
33
+ });
package/src/index.ts CHANGED
@@ -3,11 +3,12 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ export * from './active-cell-manager';
6
7
  export * from './icons';
7
8
  export * from './model';
8
9
  export * from './registry';
10
+ export * from './selection-watcher';
9
11
  export * from './types';
10
- export * from './active-cell-manager';
11
12
  export * from './widgets/chat-error';
12
13
  export * from './widgets/chat-sidebar';
13
14
  export * from './widgets/chat-widget';
package/src/model.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  IUser
16
16
  } from './types';
17
17
  import { IActiveCellManager } from './active-cell-manager';
18
+ import { ISelectionWatcher } from './selection-watcher';
18
19
 
19
20
  /**
20
21
  * The chat model interface.
@@ -55,6 +56,11 @@ export interface IChatModel extends IDisposable {
55
56
  */
56
57
  readonly activeCellManager: IActiveCellManager | null;
57
58
 
59
+ /**
60
+ * Get the selection watcher.
61
+ */
62
+ readonly selectionWatcher: ISelectionWatcher | null;
63
+
58
64
  /**
59
65
  * A signal emitting when the messages list is updated.
60
66
  */
@@ -75,6 +81,11 @@ export interface IChatModel extends IDisposable {
75
81
  */
76
82
  readonly viewportChanged?: ISignal<IChatModel, number[]>;
77
83
 
84
+ /**
85
+ * A signal emitting when the writers change.
86
+ */
87
+ readonly writersChanged?: ISignal<IChatModel, IUser[]>;
88
+
78
89
  /**
79
90
  * A signal emitting when the focus is requested on the input.
80
91
  */
@@ -87,7 +98,7 @@ export interface IChatModel extends IDisposable {
87
98
  * @param message - the message to send.
88
99
  * @returns whether the message has been sent or not, or nothing if not needed.
89
100
  */
90
- addMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
101
+ sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
91
102
 
92
103
  /**
93
104
  * Optional, to update a message from the chat panel.
@@ -145,10 +156,20 @@ export interface IChatModel extends IDisposable {
145
156
  */
146
157
  messagesDeleted(index: number, count: number): void;
147
158
 
159
+ /**
160
+ * Update the current writers list.
161
+ */
162
+ updateWriters(writers: IUser[]): void;
163
+
148
164
  /**
149
165
  * Function to request the focus on the input of the chat.
150
166
  */
151
167
  focusInput(): void;
168
+
169
+ /**
170
+ * Function called by the input on key pressed.
171
+ */
172
+ inputChanged?(input?: string): void;
152
173
  }
153
174
 
154
175
  /**
@@ -164,23 +185,19 @@ export class ChatModel implements IChatModel {
164
185
  const config = options.config ?? {};
165
186
 
166
187
  // Stack consecutive messages from the same user by default.
167
- this._config = { stackMessages: true, ...config };
188
+ this._config = {
189
+ stackMessages: true,
190
+ sendTypingNotification: true,
191
+ ...config
192
+ };
168
193
 
169
194
  this._commands = options.commands;
170
195
 
171
196
  this._activeCellManager = options.activeCellManager ?? null;
172
- }
173
197
 
174
- /**
175
- * The chat messages list.
176
- */
177
- get messages(): IChatMessage[] {
178
- return this._messages;
198
+ this._selectionWatcher = options.selectionWatcher ?? null;
179
199
  }
180
200
 
181
- get activeCellManager(): IActiveCellManager | null {
182
- return this._activeCellManager;
183
- }
184
201
  /**
185
202
  * The chat model id.
186
203
  */
@@ -201,6 +218,26 @@ export class ChatModel implements IChatModel {
201
218
  this._name = value;
202
219
  }
203
220
 
221
+ /**
222
+ * The chat messages list.
223
+ */
224
+ get messages(): IChatMessage[] {
225
+ return this._messages;
226
+ }
227
+ /**
228
+ * Get the active cell manager.
229
+ */
230
+ get activeCellManager(): IActiveCellManager | null {
231
+ return this._activeCellManager;
232
+ }
233
+
234
+ /**
235
+ * Get the selection watcher.
236
+ */
237
+ get selectionWatcher(): ISelectionWatcher | null {
238
+ return this._selectionWatcher;
239
+ }
240
+
204
241
  /**
205
242
  * Timestamp of the last read message in local storage.
206
243
  */
@@ -338,6 +375,13 @@ export class ChatModel implements IChatModel {
338
375
  return this._viewportChanged;
339
376
  }
340
377
 
378
+ /**
379
+ * A signal emitting when the writers change.
380
+ */
381
+ get writersChanged(): ISignal<IChatModel, IUser[]> {
382
+ return this._writersChanged;
383
+ }
384
+
341
385
  /**
342
386
  * A signal emitting when the focus is requested on the input.
343
387
  */
@@ -352,7 +396,7 @@ export class ChatModel implements IChatModel {
352
396
  * @param message - the message to send.
353
397
  * @returns whether the message has been sent or not.
354
398
  */
355
- addMessage(message: INewMessage): Promise<boolean | void> | boolean | void {}
399
+ sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void {}
356
400
 
357
401
  /**
358
402
  * Dispose the chat model.
@@ -452,6 +496,14 @@ export class ChatModel implements IChatModel {
452
496
  this._messagesUpdated.emit();
453
497
  }
454
498
 
499
+ /**
500
+ * Update the current writers list.
501
+ * This implementation only propagate the list via a signal.
502
+ */
503
+ updateWriters(writers: IUser[]): void {
504
+ this._writersChanged.emit(writers);
505
+ }
506
+
455
507
  /**
456
508
  * Function to request the focus on the input of the chat.
457
509
  */
@@ -459,6 +511,11 @@ export class ChatModel implements IChatModel {
459
511
  this._focusInputSignal.emit();
460
512
  }
461
513
 
514
+ /**
515
+ * Function called by the input on key pressed.
516
+ */
517
+ inputChanged?(input?: string): void {}
518
+
462
519
  /**
463
520
  * Add unread messages to the list.
464
521
  * @param indexes - list of new indexes.
@@ -517,11 +574,13 @@ export class ChatModel implements IChatModel {
517
574
  private _isDisposed = false;
518
575
  private _commands?: CommandRegistry;
519
576
  private _activeCellManager: IActiveCellManager | null;
577
+ private _selectionWatcher: ISelectionWatcher | null;
520
578
  private _notificationId: string | null = null;
521
579
  private _messagesUpdated = new Signal<IChatModel, void>(this);
522
580
  private _configChanged = new Signal<IChatModel, IConfig>(this);
523
581
  private _unreadChanged = new Signal<IChatModel, number[]>(this);
524
582
  private _viewportChanged = new Signal<IChatModel, number[]>(this);
583
+ private _writersChanged = new Signal<IChatModel, IUser[]>(this);
525
584
  private _focusInputSignal = new Signal<ChatModel, void>(this);
526
585
  }
527
586
 
@@ -544,8 +603,13 @@ export namespace ChatModel {
544
603
  commands?: CommandRegistry;
545
604
 
546
605
  /**
547
- * Active cell manager
606
+ * Active cell manager.
548
607
  */
549
608
  activeCellManager?: IActiveCellManager | null;
609
+
610
+ /**
611
+ * Selection watcher.
612
+ */
613
+ selectionWatcher?: ISelectionWatcher | null;
550
614
  }
551
615
  }