@jupyterlite/ai 0.11.1 → 0.13.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 (76) hide show
  1. package/lib/agent.d.ts +61 -7
  2. package/lib/agent.js +286 -103
  3. package/lib/chat-commands/clear.d.ts +8 -0
  4. package/lib/chat-commands/clear.js +30 -0
  5. package/lib/chat-commands/index.d.ts +2 -0
  6. package/lib/chat-commands/index.js +2 -0
  7. package/lib/chat-commands/skills.d.ts +19 -0
  8. package/lib/chat-commands/skills.js +57 -0
  9. package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
  10. package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
  11. package/lib/chat-model.d.ts +16 -0
  12. package/lib/chat-model.js +191 -11
  13. package/lib/completion/completion-provider.d.ts +1 -1
  14. package/lib/completion/completion-provider.js +14 -2
  15. package/lib/components/model-select.js +4 -4
  16. package/lib/components/tool-select.d.ts +11 -2
  17. package/lib/components/tool-select.js +77 -18
  18. package/lib/index.d.ts +3 -3
  19. package/lib/index.js +311 -72
  20. package/lib/models/settings-model.d.ts +3 -0
  21. package/lib/models/settings-model.js +63 -14
  22. package/lib/providers/built-in-providers.js +12 -7
  23. package/lib/providers/provider-tools.d.ts +36 -0
  24. package/lib/providers/provider-tools.js +93 -0
  25. package/lib/rendered-message-outputarea.d.ts +24 -0
  26. package/lib/rendered-message-outputarea.js +48 -0
  27. package/lib/skills/index.d.ts +4 -0
  28. package/lib/skills/index.js +7 -0
  29. package/lib/skills/parse-skill.d.ts +25 -0
  30. package/lib/skills/parse-skill.js +69 -0
  31. package/lib/skills/skill-loader.d.ts +25 -0
  32. package/lib/skills/skill-loader.js +133 -0
  33. package/lib/skills/skill-registry.d.ts +31 -0
  34. package/lib/skills/skill-registry.js +100 -0
  35. package/lib/skills/types.d.ts +29 -0
  36. package/lib/skills/types.js +5 -0
  37. package/lib/tokens.d.ts +77 -7
  38. package/lib/tokens.js +6 -1
  39. package/lib/tools/commands.js +4 -2
  40. package/lib/tools/skills.d.ts +9 -0
  41. package/lib/tools/skills.js +73 -0
  42. package/lib/tools/web.d.ts +8 -0
  43. package/lib/tools/web.js +196 -0
  44. package/lib/widgets/ai-settings.d.ts +1 -1
  45. package/lib/widgets/ai-settings.js +157 -38
  46. package/lib/widgets/main-area-chat.d.ts +6 -0
  47. package/lib/widgets/main-area-chat.js +28 -0
  48. package/lib/widgets/provider-config-dialog.js +207 -4
  49. package/package.json +18 -11
  50. package/schema/settings-model.json +97 -2
  51. package/src/agent.ts +397 -123
  52. package/src/chat-commands/clear.ts +46 -0
  53. package/src/chat-commands/index.ts +2 -0
  54. package/src/chat-commands/skills.ts +87 -0
  55. package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
  56. package/src/chat-model.ts +270 -23
  57. package/src/completion/completion-provider.ts +26 -12
  58. package/src/components/model-select.tsx +4 -5
  59. package/src/components/tool-select.tsx +110 -7
  60. package/src/index.ts +395 -87
  61. package/src/models/settings-model.ts +70 -15
  62. package/src/providers/built-in-providers.ts +12 -7
  63. package/src/providers/provider-tools.ts +179 -0
  64. package/src/rendered-message-outputarea.ts +62 -0
  65. package/src/skills/index.ts +14 -0
  66. package/src/skills/parse-skill.ts +91 -0
  67. package/src/skills/skill-loader.ts +175 -0
  68. package/src/skills/skill-registry.ts +137 -0
  69. package/src/skills/types.ts +37 -0
  70. package/src/tokens.ts +109 -9
  71. package/src/tools/commands.ts +4 -2
  72. package/src/tools/skills.ts +84 -0
  73. package/src/tools/web.ts +238 -0
  74. package/src/widgets/ai-settings.tsx +357 -77
  75. package/src/widgets/main-area-chat.ts +34 -1
  76. package/src/widgets/provider-config-dialog.tsx +496 -3
@@ -8,9 +8,10 @@ import Delete from '@mui/icons-material/Delete';
8
8
  import Edit from '@mui/icons-material/Edit';
9
9
  import Error from '@mui/icons-material/Error';
10
10
  import ErrorOutline from '@mui/icons-material/ErrorOutline';
11
+ import InfoOutlined from '@mui/icons-material/InfoOutlined';
11
12
  import MoreVert from '@mui/icons-material/MoreVert';
12
13
  import Settings from '@mui/icons-material/Settings';
13
- import { Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem, ListItemSecondaryAction, ListItemText, Menu, MenuItem, Select, Switch, Tab, Tabs, TextField, ThemeProvider, Typography, createTheme } from '@mui/material';
14
+ import { Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem, ListItemText, Menu, MenuItem, Select, Switch, Tab, Tabs, TextField, ThemeProvider, Tooltip, Typography, createTheme } from '@mui/material';
14
15
  import React, { useEffect, useMemo, useState } from 'react';
15
16
  import { SECRETS_NAMESPACE, SECRETS_REPLACEMENT } from '../tokens';
16
17
  import { ProviderConfigDialog } from './provider-config-dialog';
@@ -51,6 +52,11 @@ export class AISettingsWidget extends ReactWidget {
51
52
  this.title.label = this._trans.__('AI Settings');
52
53
  this.title.caption = this._trans.__('Configure AI providers and behavior');
53
54
  this.title.closable = true;
55
+ // Disable the secrets manager if the token is empty.
56
+ if (!options.token) {
57
+ this._settingsModel.updateConfig({ useSecretsManager: false });
58
+ this._secretsManager = undefined;
59
+ }
54
60
  }
55
61
  /**
56
62
  * Render the AI settings component
@@ -168,11 +174,19 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
168
174
  void promptDebouncer.invoke();
169
175
  };
170
176
  const getSecretFromManager = async (provider, fieldName) => {
171
- const secret = await secretsManager?.get(Private.getToken(), SECRETS_NAMESPACE, `${provider}:${fieldName}`);
177
+ const token = Private.getToken();
178
+ if (!token) {
179
+ return;
180
+ }
181
+ const secret = await secretsManager?.get(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`);
172
182
  return secret?.value;
173
183
  };
174
184
  const setSecretToManager = async (provider, fieldName, value) => {
175
- await secretsManager?.set(Private.getToken(), SECRETS_NAMESPACE, `${provider}:${fieldName}`, {
185
+ const token = Private.getToken();
186
+ if (!token) {
187
+ return;
188
+ }
189
+ await secretsManager?.set(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`, {
176
190
  namespace: SECRETS_NAMESPACE,
177
191
  id: `${provider}:${fieldName}`,
178
192
  value
@@ -188,7 +202,11 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
188
202
  if (!(model.config.useSecretsManager && secretsManager)) {
189
203
  return;
190
204
  }
191
- await secretsManager?.attach(Private.getToken(), SECRETS_NAMESPACE, `${provider}:${fieldName}`, input);
205
+ const token = Private.getToken();
206
+ if (!token) {
207
+ return;
208
+ }
209
+ await secretsManager?.attach(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`, input);
192
210
  };
193
211
  /**
194
212
  * Handle adding a new AI provider
@@ -270,22 +288,29 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
270
288
  if (updates.useSecretsManager !== undefined) {
271
289
  if (updates.useSecretsManager) {
272
290
  for (const provider of model.config.providers) {
273
- // if the secrets manager doesn't have the current API key, copy the current
291
+ const settingsApiKey = provider.apiKey;
292
+ // If the secrets manager doesn't have the current API key, set the current
274
293
  // one from settings.
294
+ // Update the settings value with SECRETS_REPLACEMENT if a key exist in the
295
+ // secrets manager (was already there or a value was set in settings).
275
296
  if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
276
- setSecretToManager(provider.provider, 'apiKey', provider.apiKey ?? '');
297
+ if (settingsApiKey !== undefined) {
298
+ setSecretToManager(provider.provider, 'apiKey', settingsApiKey !== SECRETS_REPLACEMENT ? settingsApiKey : '');
299
+ provider.apiKey = SECRETS_REPLACEMENT;
300
+ await model.updateProvider(provider.id, provider);
301
+ }
302
+ }
303
+ else {
304
+ provider.apiKey = SECRETS_REPLACEMENT;
305
+ await model.updateProvider(provider.id, provider);
277
306
  }
278
- provider.apiKey = SECRETS_REPLACEMENT;
279
- await model.updateProvider(provider.id, provider);
280
307
  }
281
308
  }
282
309
  else {
283
310
  for (const provider of model.config.providers) {
284
311
  const apiKey = await getSecretFromManager(provider.provider, 'apiKey');
285
- if (apiKey) {
286
- provider.apiKey = apiKey;
287
- await model.updateProvider(provider.id, provider);
288
- }
312
+ provider.apiKey = apiKey;
313
+ await model.updateProvider(provider.id, provider);
289
314
  }
290
315
  }
291
316
  }
@@ -399,7 +424,13 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
399
424
  const isActiveCompleter = config.useSameProviderForChatAndCompleter
400
425
  ? isActive
401
426
  : config.activeCompleterProvider === provider.id;
427
+ const providerInfo = providerRegistry.getProviderInfo(provider.provider);
428
+ const providerToolCapabilities = providerInfo?.providerToolCapabilities;
402
429
  const params = provider.parameters;
430
+ const webSearchEnabled = !!providerToolCapabilities?.webSearch &&
431
+ provider.customSettings?.webSearch?.enabled === true;
432
+ const webFetchEnabled = !!providerToolCapabilities?.webFetch &&
433
+ provider.customSettings?.webFetch?.enabled === true;
403
434
  return (React.createElement(ListItem, { key: provider.id, sx: {
404
435
  flexDirection: 'column',
405
436
  alignItems: 'stretch',
@@ -428,18 +459,21 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
428
459
  provider.model,
429
460
  provider.description &&
430
461
  ` • ${provider.description}`),
431
- params &&
432
- (params.temperature !== undefined ||
433
- params.maxOutputTokens !== undefined ||
434
- params.maxTurns !== undefined) && (React.createElement(Box, { sx: {
462
+ (params?.temperature !== undefined ||
463
+ params?.maxOutputTokens !== undefined ||
464
+ params?.maxTurns !== undefined ||
465
+ webSearchEnabled ||
466
+ webFetchEnabled) && (React.createElement(Box, { sx: {
435
467
  display: 'flex',
436
468
  flexWrap: 'wrap',
437
469
  gap: 1,
438
470
  mt: 1
439
471
  } },
440
- params.temperature !== undefined && (React.createElement(Chip, { label: trans.__('Temp: %1', params.temperature), size: "small", variant: "outlined" })),
441
- params.maxOutputTokens !== undefined && (React.createElement(Chip, { label: trans.__('Tokens: %1', params.maxOutputTokens), size: "small", variant: "outlined" })),
442
- params.maxTurns !== undefined && (React.createElement(Chip, { label: trans.__('Turns: %1', params.maxTurns), size: "small", variant: "outlined" }))))),
472
+ params?.temperature !== undefined && (React.createElement(Chip, { label: trans.__('Temp: %1', params.temperature), size: "small", variant: "outlined" })),
473
+ params?.maxOutputTokens !== undefined && (React.createElement(Chip, { label: trans.__('Tokens: %1', params.maxOutputTokens), size: "small", variant: "outlined" })),
474
+ params?.maxTurns !== undefined && (React.createElement(Chip, { label: trans.__('Turns: %1', params.maxTurns), size: "small", variant: "outlined" })),
475
+ webSearchEnabled && (React.createElement(Chip, { label: trans.__('Web Search'), size: "small", variant: "outlined", color: "info" })),
476
+ webFetchEnabled && (React.createElement(Chip, { label: trans.__('Web Fetch'), size: "small", variant: "outlined", color: "info" }))))),
443
477
  React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
444
478
  React.createElement(MoreVert, null)))));
445
479
  }))))),
@@ -488,22 +522,51 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
488
522
  React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: trans.__('System Prompt'), value: systemPromptValue, onChange: e => handleSystemPromptChange(e.target.value), placeholder: trans.__("Define the AI's behavior and personality..."), helperText: trans.__('Instructions that define how the AI should behave and respond') }),
489
523
  React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: trans.__('Completion System Prompt'), value: completionPromptValue, onChange: e => handleCompletionPromptChange(e.target.value), placeholder: trans.__('Define how the AI should generate code completions...'), helperText: trans.__('Instructions that define how the AI should generate code completions') }),
490
524
  React.createElement(Divider, { sx: { my: 2 } }),
525
+ React.createElement(Box, null,
526
+ React.createElement(Typography, { variant: "body1", gutterBottom: true, sx: {
527
+ display: 'inline-flex',
528
+ alignItems: 'center',
529
+ gap: 1
530
+ } },
531
+ trans.__('Skills Paths'),
532
+ React.createElement(Tooltip, { title: trans.__('Directories containing agent skills, relative to the server root. Skills are loaded from all paths; the first occurrence of a skill name takes priority.') },
533
+ React.createElement(InfoOutlined, { sx: { fontSize: 16 } }))),
534
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.skillsPaths ?? []).map((skillPath, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
535
+ const newPaths = [...config.skillsPaths];
536
+ newPaths.splice(index, 1);
537
+ handleConfigUpdate({ skillsPaths: newPaths });
538
+ }, size: "small" },
539
+ React.createElement(Delete, null)) },
540
+ React.createElement(ListItemText, { primary: skillPath }))))),
541
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add Skills Path'), placeholder: trans.__('e.g., .claude/skills'), onKeyDown: e => {
542
+ if (e.key === 'Enter') {
543
+ const value = e.target.value.trim();
544
+ if (value &&
545
+ !(config.skillsPaths ?? []).includes(value)) {
546
+ const newPaths = [
547
+ ...(config.skillsPaths ?? []),
548
+ value
549
+ ];
550
+ handleConfigUpdate({ skillsPaths: newPaths });
551
+ e.target.value = '';
552
+ }
553
+ }
554
+ }, helperText: trans.__('Press Enter to add a path. Defaults: .agents/skills, _agents/skills') })),
555
+ React.createElement(Divider, { sx: { my: 2 } }),
491
556
  React.createElement(Box, null,
492
557
  React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Requiring Approval')),
493
558
  React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Commands that require user approval before AI can execute them')),
494
- React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true },
495
- React.createElement(ListItemText, { primary: command }),
496
- React.createElement(ListItemSecondaryAction, null,
497
- React.createElement(IconButton, { onClick: () => {
498
- const newCommands = [
499
- ...config.commandsRequiringApproval
500
- ];
501
- newCommands.splice(index, 1);
502
- handleConfigUpdate({
503
- commandsRequiringApproval: newCommands
504
- });
505
- }, size: "small" },
506
- React.createElement(Delete, null))))))),
559
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
560
+ const newCommands = [
561
+ ...config.commandsRequiringApproval
562
+ ];
563
+ newCommands.splice(index, 1);
564
+ handleConfigUpdate({
565
+ commandsRequiringApproval: newCommands
566
+ });
567
+ }, size: "small" },
568
+ React.createElement(Delete, null)) },
569
+ React.createElement(ListItemText, { primary: command }))))),
507
570
  React.createElement(TextField, { fullWidth: true, label: trans.__('Add New Command'), placeholder: trans.__('e.g., notebook:run-cell'), onKeyDown: e => {
508
571
  if (e.key === 'Enter') {
509
572
  const value = e.target.value.trim();
@@ -519,7 +582,65 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
519
582
  e.target.value = '';
520
583
  }
521
584
  }
522
- }, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') })))))),
585
+ }, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') })),
586
+ React.createElement(Divider, { sx: { my: 2 } }),
587
+ React.createElement(Box, null,
588
+ React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Auto-Rendering MIME Bundles')),
589
+ React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Only these execute_command command IDs can auto-render MIME bundle outputs in chat')),
590
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.commandsAutoRenderMimeBundles ?? []).map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
591
+ const newCommands = [
592
+ ...(config.commandsAutoRenderMimeBundles ??
593
+ [])
594
+ ];
595
+ newCommands.splice(index, 1);
596
+ handleConfigUpdate({
597
+ commandsAutoRenderMimeBundles: newCommands
598
+ });
599
+ }, size: "small" },
600
+ React.createElement(Delete, null)) },
601
+ React.createElement(ListItemText, { primary: command }))))),
602
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add Auto-Render Command'), placeholder: trans.__('e.g., jupyterlab-ai-commands:execute-in-kernel'), onKeyDown: e => {
603
+ if (e.key === 'Enter') {
604
+ const value = e.target.value.trim();
605
+ const existingCommands = config.commandsAutoRenderMimeBundles ?? [];
606
+ if (value && !existingCommands.includes(value)) {
607
+ const newCommands = [...existingCommands, value];
608
+ handleConfigUpdate({
609
+ commandsAutoRenderMimeBundles: newCommands
610
+ });
611
+ e.target.value = '';
612
+ }
613
+ }
614
+ }, helperText: trans.__('Press Enter to add a command. Default: jupyterlab-ai-commands:execute-in-kernel') })),
615
+ React.createElement(Divider, { sx: { my: 2 } }),
616
+ React.createElement(Box, null,
617
+ React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Trusted MIME Types for Auto-Render')),
618
+ React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('When auto-rendering command outputs, these MIME types are marked trusted in chat')),
619
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.trustedMimeTypesForAutoRender ?? []).map((mimeType, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
620
+ const newMimeTypes = [
621
+ ...(config.trustedMimeTypesForAutoRender ??
622
+ [])
623
+ ];
624
+ newMimeTypes.splice(index, 1);
625
+ handleConfigUpdate({
626
+ trustedMimeTypesForAutoRender: newMimeTypes
627
+ });
628
+ }, size: "small" },
629
+ React.createElement(Delete, null)) },
630
+ React.createElement(ListItemText, { primary: mimeType }))))),
631
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add Trusted MIME Type'), placeholder: trans.__('e.g., text/html'), onKeyDown: e => {
632
+ if (e.key === 'Enter') {
633
+ const value = e.target.value.trim();
634
+ const existingMimeTypes = config.trustedMimeTypesForAutoRender ?? [];
635
+ if (value && !existingMimeTypes.includes(value)) {
636
+ const newMimeTypes = [...existingMimeTypes, value];
637
+ handleConfigUpdate({
638
+ trustedMimeTypesForAutoRender: newMimeTypes
639
+ });
640
+ e.target.value = '';
641
+ }
642
+ }
643
+ }, helperText: trans.__('Press Enter to add a MIME type. Default: text/html') })))))),
523
644
  activeTab === 2 && (React.createElement(Card, { elevation: 2 },
524
645
  React.createElement(CardContent, null,
525
646
  React.createElement(Box, { sx: {
@@ -533,7 +654,8 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
533
654
  React.createElement(Typography, { variant: "h6", component: "h2" }, trans.__('Remote MCP Servers'))),
534
655
  React.createElement(Button, { variant: "contained", startIcon: React.createElement(Add, null), onClick: openAddMCPDialog, size: "small" }, trans.__('Add Server'))),
535
656
  React.createElement(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 } }, trans.__("Configure remote Model Context Protocol (MCP) servers to extend the AI's capabilities with external tools and data sources.")),
536
- config.mcpServers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No MCP servers configured yet. Click "Add Server" to connect to remote MCP services.'))) : (React.createElement(List, null, config.mcpServers.map(server => (React.createElement(ListItem, { key: server.id, divider: true },
657
+ config.mcpServers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No MCP servers configured yet. Click "Add Server" to connect to remote MCP services.'))) : (React.createElement(List, null, config.mcpServers.map(server => (React.createElement(ListItem, { key: server.id, divider: true, secondaryAction: React.createElement(IconButton, { onClick: e => handleMCPMenuClick(e, server.id), size: "small" },
658
+ React.createElement(MoreVert, null)) },
537
659
  React.createElement(ListItemText, { primary: React.createElement(Box, { sx: {
538
660
  display: 'flex',
539
661
  alignItems: 'center',
@@ -550,10 +672,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
550
672
  React.createElement(Typography, { variant: "body2", color: "text.secondary" }, server.url),
551
673
  server.enabled && agentManagerFactory && (React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Status: %1', agentManagerFactory.isMCPServerConnected(server.name)
552
674
  ? trans.__('Connected')
553
- : trans.__('Connection failed'))))) }),
554
- React.createElement(ListItemSecondaryAction, null,
555
- React.createElement(IconButton, { onClick: e => handleMCPMenuClick(e, server.id), size: "small" },
556
- React.createElement(MoreVert, null))))))))))),
675
+ : trans.__('Connection failed'))))) }))))))))),
557
676
  React.createElement(ProviderConfigDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSave: editingProvider ? handleEditProvider : handleAddProvider, initialConfig: editingProvider, mode: editingProvider ? 'edit' : 'add', providerRegistry: providerRegistry, handleSecretField: handleSecretField, trans: trans }),
558
677
  React.createElement(Menu, { anchorEl: menuAnchor, open: Boolean(menuAnchor), onClose: handleMenuClose },
559
678
  React.createElement(MenuItem, { onClick: () => {
@@ -21,5 +21,11 @@ export declare class MainAreaChat extends MainAreaWidget<ChatWidget> {
21
21
  * Get the model of the chat.
22
22
  */
23
23
  get model(): AIChatModel;
24
+ /**
25
+ * Get the area of the chat.
26
+ */
27
+ get area(): string | undefined;
28
+ private _writersChanged;
24
29
  private _approvalButtons;
30
+ private _outputAreaCompat;
25
31
  }
@@ -2,6 +2,7 @@ import { CommandToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
2
2
  import { launchIcon } from '@jupyterlab/ui-components';
3
3
  import { ApprovalButtons } from '../approval-buttons';
4
4
  import { TokenUsageWidget } from '../components/token-usage-display';
5
+ import { RenderedMessageOutputAreaCompat } from '../rendered-message-outputarea';
5
6
  import { CommandIds } from '../tokens';
6
7
  /**
7
8
  * The chat as a main area widget.
@@ -34,11 +35,19 @@ export class MainAreaChat extends MainAreaWidget {
34
35
  chatPanel: this.content,
35
36
  agentManager: this.model.agentManager
36
37
  });
38
+ // Temporary compat: keep output-area CSS context for MIME renderers
39
+ // until jupyter-chat provides it natively.
40
+ this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
41
+ chatPanel: this.content
42
+ });
43
+ this.model.writersChanged.connect(this._writersChanged);
37
44
  }
38
45
  dispose() {
39
46
  super.dispose();
40
47
  // Dispose of the approval buttons widget when the chat is disposed.
41
48
  this._approvalButtons.dispose();
49
+ this._outputAreaCompat.dispose();
50
+ this.model.writersChanged.disconnect(this._writersChanged);
42
51
  }
43
52
  /**
44
53
  * Get the model of the chat.
@@ -46,5 +55,24 @@ export class MainAreaChat extends MainAreaWidget {
46
55
  get model() {
47
56
  return this.content.model;
48
57
  }
58
+ /**
59
+ * Get the area of the chat.
60
+ */
61
+ get area() {
62
+ return this.content.area;
63
+ }
64
+ _writersChanged = (_, writers) => {
65
+ // Check if AI is currently writing (streaming)
66
+ const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
67
+ if (aiWriting) {
68
+ this.content.inputToolbarRegistry?.hide('send');
69
+ this.content.inputToolbarRegistry?.show('stop');
70
+ }
71
+ else {
72
+ this.content.inputToolbarRegistry?.hide('stop');
73
+ this.content.inputToolbarRegistry?.show('send');
74
+ }
75
+ };
49
76
  _approvalButtons;
77
+ _outputAreaCompat;
50
78
  }
@@ -1,13 +1,72 @@
1
1
  import ExpandMore from '@mui/icons-material/ExpandMore';
2
+ import Delete from '@mui/icons-material/Delete';
2
3
  import Visibility from '@mui/icons-material/Visibility';
3
4
  import VisibilityOff from '@mui/icons-material/VisibilityOff';
4
- import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
5
+ import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, List, ListItem, ListItemText, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
5
6
  import React from 'react';
6
7
  /**
7
8
  * Default parameter values for provider configuration
8
9
  */
9
10
  const DEFAULT_TEMPERATURE = 0.7;
10
11
  const DEFAULT_MAX_TURNS = 25;
12
+ const DOMAIN_FIELD_MAP = {
13
+ 'webSearch.allowedDomains': {
14
+ section: 'webSearch',
15
+ key: 'allowedDomains'
16
+ },
17
+ 'webSearch.blockedDomains': {
18
+ section: 'webSearch',
19
+ key: 'blockedDomains'
20
+ },
21
+ 'webFetch.allowedDomains': {
22
+ section: 'webFetch',
23
+ key: 'allowedDomains'
24
+ },
25
+ 'webFetch.blockedDomains': {
26
+ section: 'webFetch',
27
+ key: 'blockedDomains'
28
+ }
29
+ };
30
+ function createEmptyDomainInputs() {
31
+ return {
32
+ 'webSearch.allowedDomains': '',
33
+ 'webSearch.blockedDomains': '',
34
+ 'webFetch.allowedDomains': '',
35
+ 'webFetch.blockedDomains': ''
36
+ };
37
+ }
38
+ function toRecord(value) {
39
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
40
+ return value;
41
+ }
42
+ return {};
43
+ }
44
+ function toStringArray(value) {
45
+ if (!Array.isArray(value)) {
46
+ return [];
47
+ }
48
+ return value.filter((item) => typeof item === 'string');
49
+ }
50
+ function sanitizeCustomSettingsForProvider(customSettings, capabilities) {
51
+ const result = { ...customSettings };
52
+ const webSearch = toRecord(customSettings.webSearch);
53
+ const webFetch = toRecord(customSettings.webFetch);
54
+ const supportsWebSearch = !!capabilities?.webSearch;
55
+ const supportsWebFetch = !!capabilities?.webFetch;
56
+ if (supportsWebSearch && webSearch.enabled === true) {
57
+ result.webSearch = webSearch;
58
+ }
59
+ else {
60
+ delete result.webSearch;
61
+ }
62
+ if (supportsWebFetch && webFetch.enabled === true) {
63
+ result.webFetch = webFetch;
64
+ }
65
+ else {
66
+ delete result.webFetch;
67
+ }
68
+ return result;
69
+ }
11
70
  export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mode, providerRegistry, handleSecretField, trans }) => {
12
71
  const apiKeyRef = React.useRef();
13
72
  const [name, setName] = React.useState(initialConfig?.name || '');
@@ -16,8 +75,17 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
16
75
  const [apiKey, setApiKey] = React.useState(initialConfig?.apiKey || '');
17
76
  const [baseURL, setBaseURL] = React.useState(initialConfig?.baseURL || '');
18
77
  const [showApiKey, setShowApiKey] = React.useState(false);
78
+ const [customSettings, setCustomSettings] = React.useState(initialConfig?.customSettings || {});
79
+ const [domainInputs, setDomainInputs] = React.useState(createEmptyDomainInputs());
19
80
  const [parameters, setParameters] = React.useState(initialConfig?.parameters || {});
20
81
  const [expandedAdvanced, setExpandedAdvanced] = React.useState(false);
82
+ const selectedProviderInfo = React.useMemo(() => providerRegistry.getProviderInfo(provider), [providerRegistry, provider]);
83
+ const providerToolCapabilities = selectedProviderInfo?.providerToolCapabilities;
84
+ const webSearchImplementation = providerToolCapabilities?.webSearch?.implementation;
85
+ const supportsWebSearch = !!providerToolCapabilities?.webSearch;
86
+ const supportsWebFetch = !!providerToolCapabilities?.webFetch;
87
+ const webSearchSettings = React.useMemo(() => toRecord(customSettings.webSearch), [customSettings]);
88
+ const webFetchSettings = React.useMemo(() => toRecord(customSettings.webFetch), [customSettings]);
21
89
  // Get provider options from registry
22
90
  const providerOptions = React.useMemo(() => {
23
91
  const providers = providerRegistry.providers;
@@ -45,11 +113,14 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
45
113
  setApiKey(initialConfig?.apiKey || '');
46
114
  setBaseURL(initialConfig?.baseURL || '');
47
115
  setParameters(initialConfig?.parameters || {});
116
+ setCustomSettings(initialConfig?.customSettings || {});
117
+ setDomainInputs(createEmptyDomainInputs());
48
118
  setShowApiKey(false);
49
119
  setExpandedAdvanced(false);
50
120
  }
51
121
  else {
52
122
  // Reset expanded state when dialog closes
123
+ setDomainInputs(createEmptyDomainInputs());
53
124
  setExpandedAdvanced(false);
54
125
  }
55
126
  }, [open, initialConfig]);
@@ -65,20 +136,103 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
65
136
  if (open && apiKeyRef.current) {
66
137
  handleSecretField(apiKeyRef.current, provider, 'apiKey');
67
138
  }
68
- }, [open, provider, apiKeyRef.current]);
139
+ }, [open, provider, handleSecretField]);
140
+ const updateCustomSetting = React.useCallback((section, key, value) => {
141
+ setCustomSettings(prev => {
142
+ const next = { ...prev };
143
+ const sectionSettings = { ...toRecord(next[section]) };
144
+ const shouldDelete = value === undefined ||
145
+ value === null ||
146
+ value === '' ||
147
+ (Array.isArray(value) && value.length === 0);
148
+ if (shouldDelete) {
149
+ delete sectionSettings[key];
150
+ }
151
+ else {
152
+ sectionSettings[key] = value;
153
+ }
154
+ if (Object.keys(sectionSettings).length === 0) {
155
+ delete next[section];
156
+ }
157
+ else {
158
+ next[section] = sectionSettings;
159
+ }
160
+ return next;
161
+ });
162
+ }, []);
163
+ const addDomainValue = React.useCallback((fieldId) => {
164
+ const valueToAdd = domainInputs[fieldId].trim();
165
+ if (!valueToAdd) {
166
+ return;
167
+ }
168
+ const { section, key } = DOMAIN_FIELD_MAP[fieldId];
169
+ const currentValues = toStringArray(toRecord(customSettings[section])[key]);
170
+ if (currentValues.includes(valueToAdd)) {
171
+ setDomainInputs(prev => ({
172
+ ...prev,
173
+ [fieldId]: ''
174
+ }));
175
+ return;
176
+ }
177
+ const nextValues = [...currentValues, valueToAdd];
178
+ updateCustomSetting(section, key, nextValues);
179
+ setDomainInputs(prev => ({
180
+ ...prev,
181
+ [fieldId]: ''
182
+ }));
183
+ }, [customSettings, domainInputs, updateCustomSetting]);
184
+ const removeDomainValue = React.useCallback((fieldId, valueToRemove) => {
185
+ const { section, key } = DOMAIN_FIELD_MAP[fieldId];
186
+ const currentValues = toStringArray(toRecord(customSettings[section])[key]);
187
+ const nextValues = currentValues.filter(value => value !== valueToRemove);
188
+ updateCustomSetting(section, key, nextValues.length > 0 ? nextValues : undefined);
189
+ }, [customSettings, updateCustomSetting]);
190
+ const renderDomainList = React.useCallback((fieldId, label, placeholder, values) => {
191
+ const domainValues = toStringArray(values);
192
+ return (React.createElement(Box, null,
193
+ React.createElement(Typography, { variant: "body2", gutterBottom: true }, label),
194
+ React.createElement(List, { dense: true, sx: {
195
+ mb: 1,
196
+ maxHeight: 160,
197
+ overflow: 'auto',
198
+ border: 1,
199
+ borderColor: 'divider',
200
+ borderRadius: 1
201
+ } }, domainValues.length === 0 ? (React.createElement(ListItem, null,
202
+ React.createElement(ListItemText, { secondary: trans.__('No domains added.'), slotProps: {
203
+ secondary: {
204
+ color: 'text.secondary'
205
+ }
206
+ } }))) : (domainValues.map(value => (React.createElement(ListItem, { key: value, secondaryAction: React.createElement(IconButton, { onClick: () => removeDomainValue(fieldId, value), size: "small" },
207
+ React.createElement(Delete, { fontSize: "small" })) },
208
+ React.createElement(ListItemText, { primary: value })))))),
209
+ React.createElement(TextField, { fullWidth: true, size: "small", label: trans.__('Add Domain'), value: domainInputs[fieldId], onChange: e => setDomainInputs(prev => ({
210
+ ...prev,
211
+ [fieldId]: e.target.value
212
+ })), onKeyDown: e => {
213
+ if (e.key === 'Enter') {
214
+ e.preventDefault();
215
+ addDomainValue(fieldId);
216
+ }
217
+ }, placeholder: placeholder, helperText: trans.__('Press Enter to add one domain.') })));
218
+ }, [addDomainValue, domainInputs, removeDomainValue, trans]);
69
219
  const handleSave = () => {
70
220
  if (!name.trim() || !provider || !model) {
71
221
  return;
72
222
  }
73
223
  // Only include parameters if at least one is set
74
224
  const hasParameters = Object.keys(parameters).some(key => parameters[key] !== undefined);
225
+ const sanitizedCustomSettings = sanitizeCustomSettingsForProvider(customSettings, providerToolCapabilities);
75
226
  const config = {
76
227
  name: name.trim(),
77
228
  provider: provider,
78
229
  model,
79
230
  ...(apiKey && { apiKey }),
80
231
  ...(baseURL && { baseURL }),
81
- ...(hasParameters && { parameters })
232
+ ...(hasParameters && { parameters }),
233
+ ...(Object.keys(sanitizedCustomSettings).length > 0 && {
234
+ customSettings: sanitizedCustomSettings
235
+ })
82
236
  };
83
237
  onSave(config);
84
238
  onClose();
@@ -177,7 +331,56 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
177
331
  React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: parameters.useFilterText ?? false, onChange: e => setParameters({
178
332
  ...parameters,
179
333
  useFilterText: e.target.checked
180
- }) }), label: trans.__('Use filter text') })))))),
334
+ }) }), label: trans.__('Use filter text') }),
335
+ (supportsWebSearch || supportsWebFetch) && (React.createElement(React.Fragment, null,
336
+ React.createElement(Typography, { variant: "body2", color: "text.secondary", sx: { mt: 2, mb: 1 } }, trans.__('Provider Web Tools')),
337
+ supportsWebSearch && (React.createElement(React.Fragment, null,
338
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webSearchSettings.enabled === true, onChange: e => updateCustomSetting('webSearch', 'enabled', e.target.checked) }), label: trans.__('Enable Web Search') }),
339
+ webSearchSettings.enabled === true && (React.createElement(Box, { sx: {
340
+ pl: 2,
341
+ borderLeft: 2,
342
+ borderColor: 'divider',
343
+ display: 'flex',
344
+ flexDirection: 'column',
345
+ gap: 1.5
346
+ } },
347
+ (webSearchImplementation === 'openai' ||
348
+ webSearchImplementation === 'anthropic') &&
349
+ renderDomainList('webSearch.allowedDomains', trans.__('Allowed Domains'), trans.__('example.com'), webSearchSettings.allowedDomains),
350
+ webSearchImplementation === 'openai' && (React.createElement(React.Fragment, null,
351
+ React.createElement(FormControl, { fullWidth: true },
352
+ React.createElement(InputLabel, null, trans.__('Search Context Size')),
353
+ React.createElement(Select, { value: webSearchSettings.searchContextSize ??
354
+ 'medium', label: trans.__('Search Context Size'), onChange: e => updateCustomSetting('webSearch', 'searchContextSize', e.target.value) },
355
+ React.createElement(MenuItem, { value: "low" }, trans.__('Low')),
356
+ React.createElement(MenuItem, { value: "medium" }, trans.__('Medium')),
357
+ React.createElement(MenuItem, { value: "high" }, trans.__('High')))),
358
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webSearchSettings.externalWebAccess !==
359
+ false, onChange: e => updateCustomSetting('webSearch', 'externalWebAccess', e.target.checked) }), label: trans.__('Use External Web Access') }))),
360
+ webSearchImplementation === 'anthropic' && (React.createElement(React.Fragment, null,
361
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Web Search Max Uses'), type: "number", value: webSearchSettings.maxUses ?? '', onChange: e => updateCustomSetting('webSearch', 'maxUses', e.target.value
362
+ ? Number(e.target.value)
363
+ : undefined), inputProps: { min: 1 } }),
364
+ renderDomainList('webSearch.blockedDomains', trans.__('Blocked Domains'), trans.__('spam.example.com'), webSearchSettings.blockedDomains))))))),
365
+ supportsWebFetch && (React.createElement(React.Fragment, null,
366
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webFetchSettings.enabled === true, onChange: e => updateCustomSetting('webFetch', 'enabled', e.target.checked) }), label: trans.__('Enable Web Fetch') }),
367
+ webFetchSettings.enabled === true && (React.createElement(Box, { sx: {
368
+ pl: 2,
369
+ borderLeft: 2,
370
+ borderColor: 'divider',
371
+ display: 'flex',
372
+ flexDirection: 'column',
373
+ gap: 1.5
374
+ } },
375
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Web Fetch Max Uses'), type: "number", value: webFetchSettings.maxUses ?? '', onChange: e => updateCustomSetting('webFetch', 'maxUses', e.target.value
376
+ ? Number(e.target.value)
377
+ : undefined), inputProps: { min: 1 } }),
378
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Web Fetch Max Content Tokens'), type: "number", value: webFetchSettings.maxContentTokens ?? '', onChange: e => updateCustomSetting('webFetch', 'maxContentTokens', e.target.value
379
+ ? Number(e.target.value)
380
+ : undefined), inputProps: { min: 1 } }),
381
+ renderDomainList('webFetch.allowedDomains', trans.__('Allowed Domains'), trans.__('docs.example.com'), webFetchSettings.allowedDomains),
382
+ renderDomainList('webFetch.blockedDomains', trans.__('Blocked Domains'), trans.__('spam.example.com'), webFetchSettings.blockedDomains),
383
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webFetchSettings.citationsEnabled === true, onChange: e => updateCustomSetting('webFetch', 'citationsEnabled', e.target.checked) }), label: trans.__('Enable Citations') })))))))))))),
181
384
  React.createElement(DialogActions, null,
182
385
  React.createElement(Button, { onClick: onClose }, trans.__('Cancel')),
183
386
  React.createElement(Button, { onClick: handleSave, variant: "contained", disabled: !isValid }, mode === 'add' ? trans.__('Add Provider') : trans.__('Save Changes')))));