@jupyterlite/ai 0.16.0 → 0.17.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.
package/src/chat-model.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  IChatContext,
6
6
  IMessage,
7
7
  IMessageContent,
8
+ IMimeModelBody,
8
9
  INewMessage,
9
10
  IUser
10
11
  } from '@jupyter/chat';
@@ -31,6 +32,8 @@ import { Debouncer } from '@lumino/polling';
31
32
 
32
33
  import { ISignal, Signal } from '@lumino/signaling';
33
34
 
35
+ import type { UserContent, ImagePart, FilePart, ModelMessage } from 'ai';
36
+
34
37
  import { AI_AVATAR } from './icons';
35
38
 
36
39
  import type { IAgentManager, IAISettingsModel, ITokenUsage } from './tokens';
@@ -133,6 +136,34 @@ export class AIChatModel extends AbstractChatModel {
133
136
  this.setReady();
134
137
  }
135
138
 
139
+ /**
140
+ * A signal emitting when the chat name has changed.
141
+ */
142
+ get nameChanged(): ISignal<AIChatModel, string> {
143
+ return this._nameChanged;
144
+ }
145
+
146
+ /**
147
+ * The title of the chat.
148
+ */
149
+ get title(): string | null {
150
+ return this._title;
151
+ }
152
+ set title(value: string | null) {
153
+ this._title = value;
154
+ if (this.autosave) {
155
+ this._autosaveDebouncer.invoke();
156
+ }
157
+ this._titleChanged.emit(this._title);
158
+ }
159
+
160
+ /**
161
+ * A signal emitting when the chat title has changed.
162
+ */
163
+ get titleChanged(): ISignal<AIChatModel, string | null> {
164
+ return this._titleChanged;
165
+ }
166
+
136
167
  /**
137
168
  * Whether to save the chat automatically.
138
169
  */
@@ -171,13 +202,6 @@ export class AIChatModel extends AbstractChatModel {
171
202
  return this._autosaveChanged;
172
203
  }
173
204
 
174
- /**
175
- * A signal emitting when the chat name has changed.
176
- */
177
- get nameChanged(): ISignal<AIChatModel, string> {
178
- return this._nameChanged;
179
- }
180
-
181
205
  /**
182
206
  * Gets the current user information.
183
207
  */
@@ -243,10 +267,10 @@ export class AIChatModel extends AbstractChatModel {
243
267
  /**
244
268
  * Clears all messages from the chat and resets conversation state.
245
269
  */
246
- clearMessages = (): void => {
270
+ clearMessages = async (): Promise<void> => {
247
271
  this.messagesDeleted(0, this.messages.length);
248
272
  this._toolContexts.clear();
249
- this._agentManager.clearHistory();
273
+ await this._agentManager.clearHistory();
250
274
  };
251
275
 
252
276
  /**
@@ -303,16 +327,24 @@ export class AIChatModel extends AbstractChatModel {
303
327
 
304
328
  try {
305
329
  // Process attachments and add their content to the message
306
- let enhancedMessage = message.body;
330
+ let enhancedMessage: UserContent = message.body;
307
331
  if (this.input.attachments.length > 0) {
308
- const attachmentContents = await this._processAttachments(
309
- this.input.attachments
332
+ const { textContents, binaryParts } = await Private.processAttachments(
333
+ this.input.attachments,
334
+ this.input.documentManager
310
335
  );
311
336
  this.input.clearAttachments();
312
337
 
313
- if (attachmentContents.length > 0) {
314
- enhancedMessage +=
315
- '\n\n--- Attached Files ---\n' + attachmentContents.join('\n\n');
338
+ let textPart = message.body;
339
+ if (textContents.length > 0) {
340
+ textPart +=
341
+ '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
342
+ }
343
+
344
+ if (binaryParts.length > 0) {
345
+ enhancedMessage = [{ type: 'text', text: textPart }, ...binaryParts];
346
+ } else {
347
+ enhancedMessage = textPart;
316
348
  }
317
349
  }
318
350
 
@@ -418,13 +450,39 @@ export class AIChatModel extends AbstractChatModel {
418
450
  attachments
419
451
  };
420
452
  });
421
- this.clearMessages();
453
+ await this.clearMessages();
422
454
  this.messagesInserted(0, messages);
423
455
  this._agentManager.setHistory(messages);
424
456
  this.autosave = content.metadata?.autosave ?? false;
457
+ this.title = content.metadata?.title ?? null;
425
458
  return true;
426
459
  };
427
460
 
461
+ /**
462
+ * Request a title to this chat, regarding the message history.
463
+ */
464
+ async requestTitle(): Promise<string> {
465
+ const history = this.messages
466
+ .filter(msg => msg.body !== '')
467
+ .map(
468
+ msg =>
469
+ `${msg.sender.username === 'ai-assistant' ? 'assistant' : 'user'}: ${msg.body}`
470
+ )
471
+ .join('\n');
472
+ const messages: ModelMessage[] = [
473
+ {
474
+ role: 'system',
475
+ content:
476
+ "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting. Focus on the user's main intent."
477
+ },
478
+ {
479
+ role: 'user',
480
+ content: history
481
+ }
482
+ ];
483
+ return this.agentManager.textResponse(messages);
484
+ }
485
+
428
486
  /**
429
487
  * Serialize the model for backup
430
488
  */
@@ -474,7 +532,8 @@ export class AIChatModel extends AbstractChatModel {
474
532
  attachments,
475
533
  metadata: {
476
534
  provider,
477
- autosave: this.autosave
535
+ autosave: this.autosave,
536
+ ...(this.title ? { title: this.title } : {})
478
537
  }
479
538
  };
480
539
  }
@@ -862,61 +921,286 @@ export class AIChatModel extends AbstractChatModel {
862
921
  });
863
922
  }
864
923
 
924
+ // Private fields
925
+ private _settingsModel: IAISettingsModel;
926
+ private _user: IUser;
927
+ private _toolContexts: Map<string, IToolExecutionContext> = new Map();
928
+ private _agentManager: IAgentManager;
929
+ private _currentStreamingMessage: IMessage | null = null;
930
+ private _nameChanged = new Signal<AIChatModel, string>(this);
931
+ private _contentsManager?: Contents.IManager;
932
+ private _autosave: boolean = false;
933
+ private _autosaveChanged = new Signal<AIChatModel, boolean>(this);
934
+ private _autosaveDebouncer: Debouncer;
935
+ private _title: string | null = null;
936
+ private _titleChanged = new Signal<AIChatModel, string | null>(this);
937
+ }
938
+
939
+ namespace Private {
940
+ type IDisplayOutput =
941
+ | nbformat.IDisplayData
942
+ | nbformat.IDisplayUpdate
943
+ | nbformat.IExecuteResult;
944
+
945
+ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
946
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
947
+ };
948
+
949
+ const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
950
+ if (!isPlainObject(value)) {
951
+ return false;
952
+ }
953
+
954
+ const output = value as nbformat.IOutput;
955
+ return (
956
+ nbformat.isDisplayData(output) ||
957
+ nbformat.isDisplayUpdate(output) ||
958
+ nbformat.isExecuteResult(output)
959
+ );
960
+ };
961
+
962
+ const toMimeBundle = (
963
+ value: IDisplayOutput,
964
+ trustedMimeTypes: ReadonlySet<string>
965
+ ): IMimeModelBody | null => {
966
+ const data = value.data;
967
+ if (!isPlainObject(data) || Object.keys(data).length === 0) {
968
+ return null;
969
+ }
970
+
971
+ return {
972
+ data: data as IRenderMime.IMimeModel['data'],
973
+ ...(isPlainObject(value.metadata)
974
+ ? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
975
+ : {}),
976
+ // MIME auto-rendering only runs for explicitly configured command IDs.
977
+ // Trust handling is configurable to keep risky MIME execution opt-in.
978
+ ...(Object.keys(data).some(m => trustedMimeTypes.has(m))
979
+ ? { trusted: true }
980
+ : {})
981
+ };
982
+ };
983
+
984
+ /**
985
+ * Normalize arbitrary tool payloads into canonical display outputs.
986
+ *
987
+ * Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
988
+ * often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
989
+ */
990
+ const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
991
+ if (isDisplayOutput(value)) {
992
+ return [value];
993
+ }
994
+
995
+ if (Array.isArray(value)) {
996
+ return value.filter(isDisplayOutput);
997
+ }
998
+
999
+ if (!isPlainObject(value)) {
1000
+ return [];
1001
+ }
1002
+
1003
+ if (Array.isArray(value.outputs)) {
1004
+ return value.outputs.filter(isDisplayOutput);
1005
+ }
1006
+
1007
+ if ('result' in value) {
1008
+ return toDisplayOutputs(value.result);
1009
+ }
1010
+
1011
+ return [];
1012
+ };
1013
+
1014
+ /**
1015
+ * Extract rendermime-ready mime bundles from arbitrary tool results.
1016
+ */
1017
+ export function extractMimeBundlesFromUnknown(
1018
+ content: unknown,
1019
+ options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
1020
+ ): IMimeModelBody[] {
1021
+ const bundles: IMimeModelBody[] = [];
1022
+ const outputs = toDisplayOutputs(content);
1023
+ const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
1024
+ for (const output of outputs) {
1025
+ const bundle = toMimeBundle(output, trustedMimeTypes);
1026
+ if (bundle) {
1027
+ bundles.push(bundle);
1028
+ }
1029
+ }
1030
+ return bundles;
1031
+ }
1032
+
1033
+ export function formatToolOutput(outputData: unknown): string {
1034
+ if (typeof outputData === 'string') {
1035
+ return outputData;
1036
+ }
1037
+
1038
+ try {
1039
+ return JSON.stringify(outputData, null, 2);
1040
+ } catch {
1041
+ return '[Complex object - cannot serialize]';
1042
+ }
1043
+ }
1044
+
865
1045
  /**
866
- * Processes file attachments and returns their content as formatted strings.
1046
+ * Processes file attachments and returns text contents and binary parts separately.
867
1047
  * @param attachments Array of file attachments to process
868
- * @returns Array of formatted attachment contents
1048
+ * @param documentManager Optional document manager for file operations
1049
+ * @returns Text contents and binary parts
869
1050
  */
870
- private async _processAttachments(
871
- attachments: IAttachment[]
872
- ): Promise<string[]> {
873
- const contents: string[] = [];
1051
+ export async function processAttachments(
1052
+ attachments: IAttachment[],
1053
+ documentManager: IDocumentManager | null | undefined
1054
+ ): Promise<{
1055
+ textContents: string[];
1056
+ binaryParts: Array<ImagePart | FilePart>;
1057
+ }> {
1058
+ const textContents: string[] = [];
1059
+ const binaryParts: Array<ImagePart | FilePart> = [];
1060
+
1061
+ if (!documentManager) {
1062
+ return { textContents, binaryParts };
1063
+ }
874
1064
 
875
1065
  for (const attachment of attachments) {
876
1066
  try {
877
1067
  if (attachment.type === 'notebook' && attachment.cells?.length) {
878
- const cellContents = await this._readNotebookCells(attachment);
1068
+ const cellContents = await readNotebookCells(
1069
+ attachment,
1070
+ documentManager
1071
+ );
879
1072
  if (cellContents) {
880
- contents.push(cellContents);
1073
+ textContents.push(cellContents);
881
1074
  }
882
1075
  } else {
883
- const fileContent = await this._readFileAttachment(attachment);
884
- if (fileContent) {
885
- const fileExtension = PathExt.extname(
886
- attachment.value
887
- ).toLowerCase();
888
- const language = fileExtension === '.ipynb' ? 'json' : '';
889
- contents.push(
890
- `**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``
1076
+ let mimetype = attachment.mimetype;
1077
+ const fileExtension = PathExt.extname(attachment.value).toLowerCase();
1078
+
1079
+ // Fetch mimetype from server metadata if not provided
1080
+ if (!mimetype) {
1081
+ try {
1082
+ const diskModel = await documentManager.services.contents.get(
1083
+ attachment.value,
1084
+ { content: false }
1085
+ );
1086
+ mimetype = diskModel?.mimetype;
1087
+ } catch (e) {
1088
+ console.warn(
1089
+ `Failed to fetch metadata for ${attachment.value}:`,
1090
+ e
1091
+ );
1092
+ }
1093
+ }
1094
+
1095
+ if (mimetype?.startsWith('image/')) {
1096
+ const data = await readBinaryAttachment(
1097
+ attachment,
1098
+ documentManager
1099
+ );
1100
+ if (data) {
1101
+ binaryParts.push({
1102
+ type: 'image',
1103
+ image: data,
1104
+ mediaType: mimetype
1105
+ });
1106
+ }
1107
+ } else if (mimetype === 'application/pdf') {
1108
+ const data = await readBinaryAttachment(
1109
+ attachment,
1110
+ documentManager
891
1111
  );
1112
+ if (data) {
1113
+ binaryParts.push({
1114
+ type: 'file',
1115
+ data,
1116
+ mediaType: mimetype,
1117
+ filename: PathExt.basename(attachment.value)
1118
+ });
1119
+ }
1120
+ } else {
1121
+ const fileContent = await readFileAttachment(
1122
+ attachment,
1123
+ documentManager
1124
+ );
1125
+ if (fileContent) {
1126
+ const language =
1127
+ fileExtension === '.ipynb' ||
1128
+ mimetype === 'application/x-ipynb+json'
1129
+ ? 'json'
1130
+ : '';
1131
+ textContents.push(
1132
+ `**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``
1133
+ );
1134
+ }
892
1135
  }
893
1136
  }
894
1137
  } catch (error) {
895
1138
  console.warn(`Failed to read attachment ${attachment.value}:`, error);
896
- contents.push(`**File: ${attachment.value}** (Could not read file)`);
1139
+ textContents.push(
1140
+ `**File: ${attachment.value}** (Could not read file)`
1141
+ );
897
1142
  }
898
1143
  }
899
1144
 
900
- return contents;
1145
+ return { textContents, binaryParts };
1146
+ }
1147
+
1148
+ /**
1149
+ * Reads a binary attachment and returns its base64-encoded content.
1150
+ * @param attachment The attachment to read
1151
+ * @param documentManager Optional document manager for file operations
1152
+ * @returns Base64 string or null if unable to read
1153
+ */
1154
+ export async function readBinaryAttachment(
1155
+ attachment: IAttachment,
1156
+ documentManager: IDocumentManager | null | undefined
1157
+ ): Promise<string | null> {
1158
+ if (!documentManager) {
1159
+ return null;
1160
+ }
1161
+
1162
+ try {
1163
+ const diskModel = await documentManager.services.contents.get(
1164
+ attachment.value,
1165
+ { content: true }
1166
+ );
1167
+ if (diskModel?.content && diskModel.format === 'base64') {
1168
+ // Strip whitespace/newlines
1169
+ return (diskModel.content as string).replace(/\s/g, '');
1170
+ }
1171
+ return null;
1172
+ } catch (error) {
1173
+ console.warn(
1174
+ `Failed to read binary attachment ${attachment.value}:`,
1175
+ error
1176
+ );
1177
+ return null;
1178
+ }
901
1179
  }
902
1180
 
903
1181
  /**
904
1182
  * Reads the content of a notebook cell.
905
1183
  * @param attachment The notebook attachment to read
1184
+ * @param documentManager Optional document manager for file operations
906
1185
  * @returns Cell content as string or null if unable to read
907
1186
  */
908
- private async _readNotebookCells(
909
- attachment: IAttachment
1187
+ export async function readNotebookCells(
1188
+ attachment: IAttachment,
1189
+ documentManager: IDocumentManager | null | undefined
910
1190
  ): Promise<string | null> {
911
- if (attachment.type !== 'notebook' || !attachment.cells) {
1191
+ if (
1192
+ attachment.type !== 'notebook' ||
1193
+ !attachment.cells ||
1194
+ !documentManager
1195
+ ) {
912
1196
  return null;
913
1197
  }
914
1198
 
915
1199
  try {
916
1200
  // Try reading from live notebook if open
917
- const widget = this.input.documentManager?.findWidget(
918
- attachment.value
919
- ) as IDocumentWidget<Notebook, INotebookModel> | undefined;
1201
+ const widget = documentManager.findWidget(attachment.value) as
1202
+ | IDocumentWidget<Notebook, INotebookModel>
1203
+ | undefined;
920
1204
  let cellData: nbformat.ICell[];
921
1205
  let kernelLang = 'text';
922
1206
 
@@ -935,7 +1219,7 @@ export class AIChatModel extends AbstractChatModel {
935
1219
  kernelLang = String(lang);
936
1220
  } else {
937
1221
  // Fallback: reading from disk
938
- const model = await this.input.documentManager?.services.contents.get(
1222
+ const model = await documentManager.services.contents.get(
939
1223
  attachment.value
940
1224
  );
941
1225
  if (!model || model.type !== 'notebook') {
@@ -1079,21 +1363,26 @@ export class AIChatModel extends AbstractChatModel {
1079
1363
  /**
1080
1364
  * Reads the content of a file attachment.
1081
1365
  * @param attachment The file attachment to read
1366
+ * @param documentManager Optional document manager for file operations
1082
1367
  * @returns File content as string or null if unable to read
1083
1368
  */
1084
- private async _readFileAttachment(
1085
- attachment: IAttachment
1369
+ export async function readFileAttachment(
1370
+ attachment: IAttachment,
1371
+ documentManager: IDocumentManager | null | undefined
1086
1372
  ): Promise<string | null> {
1087
1373
  // Handle both 'file' and 'notebook' types since both have a 'value' path
1088
- if (attachment.type !== 'file' && attachment.type !== 'notebook') {
1374
+ if (
1375
+ (attachment.type !== 'file' && attachment.type !== 'notebook') ||
1376
+ !documentManager
1377
+ ) {
1089
1378
  return null;
1090
1379
  }
1091
1380
 
1092
1381
  try {
1093
1382
  // Try reading from an open widget first
1094
- const widget = this.input.documentManager?.findWidget(
1095
- attachment.value
1096
- ) as IDocumentWidget<Notebook, INotebookModel> | undefined;
1383
+ const widget = documentManager.findWidget(attachment.value) as
1384
+ | IDocumentWidget<Notebook, INotebookModel>
1385
+ | undefined;
1097
1386
 
1098
1387
  if (widget && widget.context && widget.context.model) {
1099
1388
  const model = widget.context.model;
@@ -1108,7 +1397,7 @@ export class AIChatModel extends AbstractChatModel {
1108
1397
  }
1109
1398
 
1110
1399
  // If not open, load from disk
1111
- const diskModel = await this.input.documentManager?.services.contents.get(
1400
+ const diskModel = await documentManager.services.contents.get(
1112
1401
  attachment.value
1113
1402
  );
1114
1403
 
@@ -1139,127 +1428,6 @@ export class AIChatModel extends AbstractChatModel {
1139
1428
  return null;
1140
1429
  }
1141
1430
  }
1142
-
1143
- // Private fields
1144
- private _settingsModel: IAISettingsModel;
1145
- private _user: IUser;
1146
- private _toolContexts: Map<string, IToolExecutionContext> = new Map();
1147
- private _agentManager: IAgentManager;
1148
- private _currentStreamingMessage: IMessage | null = null;
1149
- private _nameChanged = new Signal<AIChatModel, string>(this);
1150
- private _contentsManager?: Contents.IManager;
1151
- private _autosave: boolean = false;
1152
- private _autosaveChanged = new Signal<AIChatModel, boolean>(this);
1153
- private _autosaveDebouncer: Debouncer;
1154
- }
1155
-
1156
- namespace Private {
1157
- type IMimeBody = Partial<IRenderMime.IMimeModel> &
1158
- Pick<IRenderMime.IMimeModel, 'data'>;
1159
- type IDisplayOutput =
1160
- | nbformat.IDisplayData
1161
- | nbformat.IDisplayUpdate
1162
- | nbformat.IExecuteResult;
1163
-
1164
- const isPlainObject = (value: unknown): value is Record<string, unknown> => {
1165
- return typeof value === 'object' && value !== null && !Array.isArray(value);
1166
- };
1167
-
1168
- const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
1169
- if (!isPlainObject(value)) {
1170
- return false;
1171
- }
1172
-
1173
- const output = value as nbformat.IOutput;
1174
- return (
1175
- nbformat.isDisplayData(output) ||
1176
- nbformat.isDisplayUpdate(output) ||
1177
- nbformat.isExecuteResult(output)
1178
- );
1179
- };
1180
-
1181
- const toMimeBundle = (
1182
- value: IDisplayOutput,
1183
- trustedMimeTypes: ReadonlySet<string>
1184
- ): IMimeBody | null => {
1185
- const data = value.data;
1186
- if (!isPlainObject(data) || Object.keys(data).length === 0) {
1187
- return null;
1188
- }
1189
-
1190
- return {
1191
- data: data as IRenderMime.IMimeModel['data'],
1192
- ...(isPlainObject(value.metadata)
1193
- ? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
1194
- : {}),
1195
- // MIME auto-rendering only runs for explicitly configured command IDs.
1196
- // Trust handling is configurable to keep risky MIME execution opt-in.
1197
- ...(Object.keys(data).some(m => trustedMimeTypes.has(m))
1198
- ? { trusted: true }
1199
- : {})
1200
- };
1201
- };
1202
-
1203
- /**
1204
- * Normalize arbitrary tool payloads into canonical display outputs.
1205
- *
1206
- * Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
1207
- * often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
1208
- */
1209
- const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
1210
- if (isDisplayOutput(value)) {
1211
- return [value];
1212
- }
1213
-
1214
- if (Array.isArray(value)) {
1215
- return value.filter(isDisplayOutput);
1216
- }
1217
-
1218
- if (!isPlainObject(value)) {
1219
- return [];
1220
- }
1221
-
1222
- if (Array.isArray(value.outputs)) {
1223
- return value.outputs.filter(isDisplayOutput);
1224
- }
1225
-
1226
- if ('result' in value) {
1227
- return toDisplayOutputs(value.result);
1228
- }
1229
-
1230
- return [];
1231
- };
1232
-
1233
- /**
1234
- * Extract rendermime-ready mime bundles from arbitrary tool results.
1235
- */
1236
- export function extractMimeBundlesFromUnknown(
1237
- content: unknown,
1238
- options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
1239
- ): IMimeBody[] {
1240
- const bundles: IMimeBody[] = [];
1241
- const outputs = toDisplayOutputs(content);
1242
- const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
1243
- for (const output of outputs) {
1244
- const bundle = toMimeBundle(output, trustedMimeTypes);
1245
- if (bundle) {
1246
- bundles.push(bundle);
1247
- }
1248
- }
1249
- return bundles;
1250
- }
1251
-
1252
- export function formatToolOutput(outputData: unknown): string {
1253
- if (typeof outputData === 'string') {
1254
- return outputData;
1255
- }
1256
-
1257
- try {
1258
- return JSON.stringify(outputData, null, 2);
1259
- } catch {
1260
- return '[Complex object - cannot serialize]';
1261
- }
1262
- }
1263
1431
  }
1264
1432
 
1265
1433
  /**
@@ -1311,7 +1479,7 @@ export namespace AIChatModel {
1311
1479
  /**
1312
1480
  * The clear messages callback.
1313
1481
  */
1314
- clearMessages: () => void;
1482
+ clearMessages: () => Promise<void>;
1315
1483
  /**
1316
1484
  * Adds an assistant/system message to the chat.
1317
1485
  */
@@ -1350,6 +1518,10 @@ export namespace AIChatModel {
1350
1518
  * Whether the chat is automatically saved.
1351
1519
  */
1352
1520
  autosave?: boolean;
1521
+ /**
1522
+ * An optional title of the chat.
1523
+ */
1524
+ title?: string;
1353
1525
  };
1354
1526
  };
1355
1527
  }
@@ -16,7 +16,7 @@ export interface IClearButtonProps
16
16
  /**
17
17
  * The function to clear messages.
18
18
  */
19
- clearMessages: () => void;
19
+ clearMessages: () => Promise<void>;
20
20
  /**
21
21
  * The application language translator.
22
22
  */
@@ -53,8 +53,8 @@ export function clearItem(
53
53
  return {
54
54
  element: (props: InputToolbarRegistry.IToolbarItemProps) => {
55
55
  const { model } = props;
56
- const clearMessages = () =>
57
- (model.chatContext as AIChatModel.IAIChatContext).clearMessages();
56
+ const clearMessages = async () =>
57
+ await (model.chatContext as AIChatModel.IAIChatContext).clearMessages();
58
58
  const clearProps: IClearButtonProps = {
59
59
  ...props,
60
60
  clearMessages,