@jupyterlite/ai 0.8.0 → 0.9.0-a0

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 (162) hide show
  1. package/lib/agent.d.ts +233 -0
  2. package/lib/agent.js +604 -0
  3. package/lib/chat-model.d.ts +195 -0
  4. package/lib/chat-model.js +590 -0
  5. package/lib/completion/completion-provider.d.ts +83 -0
  6. package/lib/completion/completion-provider.js +209 -0
  7. package/lib/completion/index.d.ts +1 -0
  8. package/lib/completion/index.js +1 -0
  9. package/lib/components/clear-button.d.ts +18 -0
  10. package/lib/components/clear-button.js +31 -0
  11. package/lib/components/index.d.ts +3 -0
  12. package/lib/components/index.js +3 -0
  13. package/lib/components/model-select.d.ts +19 -0
  14. package/lib/components/model-select.js +154 -0
  15. package/lib/components/stop-button.d.ts +3 -3
  16. package/lib/components/stop-button.js +8 -9
  17. package/lib/components/token-usage-display.d.ts +45 -0
  18. package/lib/components/token-usage-display.js +74 -0
  19. package/lib/components/tool-select.d.ts +27 -0
  20. package/lib/components/tool-select.js +130 -0
  21. package/lib/icons.d.ts +3 -1
  22. package/lib/icons.js +10 -13
  23. package/lib/index.d.ts +4 -5
  24. package/lib/index.js +322 -167
  25. package/lib/mcp/browser.d.ts +68 -0
  26. package/lib/mcp/browser.js +132 -0
  27. package/lib/models/settings-model.d.ts +69 -0
  28. package/lib/models/settings-model.js +295 -0
  29. package/lib/providers/built-in-providers.d.ts +9 -0
  30. package/lib/providers/built-in-providers.js +192 -0
  31. package/lib/providers/models.d.ts +37 -0
  32. package/lib/providers/models.js +28 -0
  33. package/lib/providers/provider-registry.d.ts +94 -0
  34. package/lib/providers/provider-registry.js +155 -0
  35. package/lib/tokens.d.ts +157 -86
  36. package/lib/tokens.js +16 -12
  37. package/lib/tools/commands.d.ts +11 -0
  38. package/lib/tools/commands.js +126 -0
  39. package/lib/tools/file.d.ts +27 -0
  40. package/lib/tools/file.js +262 -0
  41. package/lib/tools/notebook.d.ts +40 -0
  42. package/lib/tools/notebook.js +762 -0
  43. package/lib/tools/tool-registry.d.ts +35 -0
  44. package/lib/tools/tool-registry.js +55 -0
  45. package/lib/widgets/ai-settings.d.ts +39 -0
  46. package/lib/widgets/ai-settings.js +506 -0
  47. package/lib/widgets/chat-wrapper.d.ts +144 -0
  48. package/lib/widgets/chat-wrapper.js +390 -0
  49. package/lib/widgets/provider-config-dialog.d.ts +13 -0
  50. package/lib/widgets/provider-config-dialog.js +104 -0
  51. package/package.json +150 -41
  52. package/schema/settings-model.json +153 -0
  53. package/src/agent.ts +800 -0
  54. package/src/chat-model.ts +770 -0
  55. package/src/completion/completion-provider.ts +308 -0
  56. package/src/completion/index.ts +1 -0
  57. package/src/components/clear-button.tsx +56 -0
  58. package/src/components/index.ts +3 -0
  59. package/src/components/model-select.tsx +245 -0
  60. package/src/components/stop-button.tsx +11 -11
  61. package/src/components/token-usage-display.tsx +130 -0
  62. package/src/components/tool-select.tsx +218 -0
  63. package/src/icons.ts +12 -14
  64. package/src/index.ts +468 -238
  65. package/src/mcp/browser.ts +213 -0
  66. package/src/models/settings-model.ts +409 -0
  67. package/src/providers/built-in-providers.ts +216 -0
  68. package/src/providers/models.ts +79 -0
  69. package/src/providers/provider-registry.ts +189 -0
  70. package/src/tokens.ts +203 -90
  71. package/src/tools/commands.ts +151 -0
  72. package/src/tools/file.ts +307 -0
  73. package/src/tools/notebook.ts +964 -0
  74. package/src/tools/tool-registry.ts +63 -0
  75. package/src/types.d.ts +4 -0
  76. package/src/widgets/ai-settings.tsx +1100 -0
  77. package/src/widgets/chat-wrapper.tsx +543 -0
  78. package/src/widgets/provider-config-dialog.tsx +256 -0
  79. package/style/base.css +335 -14
  80. package/style/icons/jupyternaut-lite.svg +1 -1
  81. package/lib/base-completer.d.ts +0 -49
  82. package/lib/base-completer.js +0 -14
  83. package/lib/chat-handler.d.ts +0 -56
  84. package/lib/chat-handler.js +0 -201
  85. package/lib/completion-provider.d.ts +0 -34
  86. package/lib/completion-provider.js +0 -32
  87. package/lib/default-prompts.d.ts +0 -2
  88. package/lib/default-prompts.js +0 -31
  89. package/lib/default-providers/Anthropic/completer.d.ts +0 -12
  90. package/lib/default-providers/Anthropic/completer.js +0 -46
  91. package/lib/default-providers/Anthropic/settings-schema.json +0 -70
  92. package/lib/default-providers/ChromeAI/completer.d.ts +0 -12
  93. package/lib/default-providers/ChromeAI/completer.js +0 -56
  94. package/lib/default-providers/ChromeAI/instructions.d.ts +0 -6
  95. package/lib/default-providers/ChromeAI/instructions.js +0 -42
  96. package/lib/default-providers/ChromeAI/settings-schema.json +0 -18
  97. package/lib/default-providers/Gemini/completer.d.ts +0 -12
  98. package/lib/default-providers/Gemini/completer.js +0 -48
  99. package/lib/default-providers/Gemini/instructions.d.ts +0 -2
  100. package/lib/default-providers/Gemini/instructions.js +0 -9
  101. package/lib/default-providers/Gemini/settings-schema.json +0 -64
  102. package/lib/default-providers/MistralAI/completer.d.ts +0 -13
  103. package/lib/default-providers/MistralAI/completer.js +0 -52
  104. package/lib/default-providers/MistralAI/instructions.d.ts +0 -2
  105. package/lib/default-providers/MistralAI/instructions.js +0 -18
  106. package/lib/default-providers/MistralAI/settings-schema.json +0 -75
  107. package/lib/default-providers/Ollama/completer.d.ts +0 -12
  108. package/lib/default-providers/Ollama/completer.js +0 -43
  109. package/lib/default-providers/Ollama/instructions.d.ts +0 -2
  110. package/lib/default-providers/Ollama/instructions.js +0 -70
  111. package/lib/default-providers/Ollama/settings-schema.json +0 -143
  112. package/lib/default-providers/OpenAI/completer.d.ts +0 -12
  113. package/lib/default-providers/OpenAI/completer.js +0 -43
  114. package/lib/default-providers/OpenAI/settings-schema.json +0 -628
  115. package/lib/default-providers/WebLLM/completer.d.ts +0 -21
  116. package/lib/default-providers/WebLLM/completer.js +0 -127
  117. package/lib/default-providers/WebLLM/instructions.d.ts +0 -6
  118. package/lib/default-providers/WebLLM/instructions.js +0 -32
  119. package/lib/default-providers/WebLLM/settings-schema.json +0 -19
  120. package/lib/default-providers/index.d.ts +0 -2
  121. package/lib/default-providers/index.js +0 -179
  122. package/lib/provider.d.ts +0 -144
  123. package/lib/provider.js +0 -412
  124. package/lib/settings/base.json +0 -7
  125. package/lib/settings/index.d.ts +0 -3
  126. package/lib/settings/index.js +0 -3
  127. package/lib/settings/panel.d.ts +0 -226
  128. package/lib/settings/panel.js +0 -510
  129. package/lib/settings/textarea.d.ts +0 -2
  130. package/lib/settings/textarea.js +0 -18
  131. package/lib/settings/utils.d.ts +0 -2
  132. package/lib/settings/utils.js +0 -4
  133. package/lib/types/ai-model.d.ts +0 -24
  134. package/lib/types/ai-model.js +0 -5
  135. package/schema/chat.json +0 -28
  136. package/schema/provider-registry.json +0 -29
  137. package/schema/system-prompts.json +0 -22
  138. package/src/base-completer.ts +0 -75
  139. package/src/chat-handler.ts +0 -262
  140. package/src/completion-provider.ts +0 -64
  141. package/src/default-prompts.ts +0 -33
  142. package/src/default-providers/Anthropic/completer.ts +0 -59
  143. package/src/default-providers/ChromeAI/completer.ts +0 -73
  144. package/src/default-providers/ChromeAI/instructions.ts +0 -45
  145. package/src/default-providers/Gemini/completer.ts +0 -61
  146. package/src/default-providers/Gemini/instructions.ts +0 -9
  147. package/src/default-providers/MistralAI/completer.ts +0 -69
  148. package/src/default-providers/MistralAI/instructions.ts +0 -18
  149. package/src/default-providers/Ollama/completer.ts +0 -54
  150. package/src/default-providers/Ollama/instructions.ts +0 -70
  151. package/src/default-providers/OpenAI/completer.ts +0 -54
  152. package/src/default-providers/WebLLM/completer.ts +0 -151
  153. package/src/default-providers/WebLLM/instructions.ts +0 -33
  154. package/src/default-providers/index.ts +0 -211
  155. package/src/global.d.ts +0 -9
  156. package/src/provider.ts +0 -514
  157. package/src/settings/index.ts +0 -3
  158. package/src/settings/panel.tsx +0 -773
  159. package/src/settings/textarea.tsx +0 -33
  160. package/src/settings/utils.ts +0 -5
  161. package/src/types/ai-model.ts +0 -37
  162. package/src/types/service-worker.d.ts +0 -6
@@ -0,0 +1,213 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ /**
3
+ * Browser-compatible MCP Server implementation
4
+ *
5
+ * This is a custom implementation that works around the limitation in
6
+ * @openai/agents where MCPServerStreamableHttp doesn't work in browsers
7
+ */
8
+
9
+ // Type definitions matching openai/agents MCPServer interface
10
+ interface MCPServer {
11
+ cacheToolsList: boolean;
12
+ toolFilter?: any;
13
+ connect(): Promise<void>;
14
+ readonly name: string;
15
+ close(): Promise<void>;
16
+ listTools(): Promise<MCPTool[]>;
17
+ callTool(
18
+ toolName: string,
19
+ args: Record<string, unknown> | null
20
+ ): Promise<CallToolResultContent>;
21
+ invalidateToolsCache(): Promise<void>;
22
+ }
23
+
24
+ interface MCPTool {
25
+ name: string;
26
+ description?: string;
27
+ inputSchema: {
28
+ type: 'object';
29
+ properties: Record<string, any>;
30
+ required: string[];
31
+ additionalProperties: boolean;
32
+ };
33
+ }
34
+
35
+ // CallToolResultContent is an array of content items
36
+ type CallToolResultContent = Array<{ type: string; text: string }>;
37
+
38
+ interface MCPServerStreamableHttpOptions {
39
+ url: string;
40
+ cacheToolsList?: boolean;
41
+ clientSessionTimeoutSeconds?: number;
42
+ name?: string;
43
+ logger?: any;
44
+ toolFilter?: any;
45
+ timeout?: number;
46
+ authProvider?: any;
47
+ requestInit?: any;
48
+ fetch?: any;
49
+ reconnectionOptions?: any;
50
+ sessionId?: string;
51
+ }
52
+
53
+ /**
54
+ * Browser-compatible MCP Server implementation that works around limitations
55
+ * in @openai/agents where MCPServerStreamableHttp doesn't work in browsers.
56
+ *
57
+ * This class provides a streamable HTTP client transport for MCP (Model Context Protocol)
58
+ * servers that can be used in browser environments.
59
+ */
60
+ export class BrowserMCPServerStreamableHttp implements MCPServer {
61
+ readonly name: string;
62
+ readonly cacheToolsList: boolean;
63
+ readonly toolFilter: any = undefined;
64
+
65
+ constructor(options: MCPServerStreamableHttpOptions) {
66
+ this._options = options;
67
+ this.name = options.name || `browser-mcp-server: ${options.url}`;
68
+ this.cacheToolsList = options.cacheToolsList ?? false;
69
+ }
70
+
71
+ async connect(): Promise<void> {
72
+ try {
73
+ // Dynamic import to handle cases where MCP SDK isn't available
74
+ const { StreamableHTTPClientTransport } = await import(
75
+ '@modelcontextprotocol/sdk/client/streamableHttp.js'
76
+ );
77
+ const { Client } = await import(
78
+ '@modelcontextprotocol/sdk/client/index.js'
79
+ );
80
+
81
+ this._transport = new StreamableHTTPClientTransport(
82
+ new URL(this._options.url),
83
+ {
84
+ authProvider: this._options.authProvider,
85
+ requestInit: this._options.requestInit,
86
+ fetch: this._options.fetch || fetch,
87
+ reconnectionOptions: this._options.reconnectionOptions,
88
+ sessionId: this._options.sessionId
89
+ }
90
+ );
91
+
92
+ this._session = new Client({
93
+ name: this.name,
94
+ version: '1.0.0'
95
+ });
96
+
97
+ await this._session.connect(this._transport);
98
+ } catch (error) {
99
+ console.error('Error initializing MCP server:', error);
100
+ await this.close();
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ async close(): Promise<void> {
106
+ if (this._session) {
107
+ try {
108
+ await this._session.close();
109
+ } catch (error) {
110
+ console.error('Error closing MCP server session:', error);
111
+ }
112
+ this._session = null;
113
+ }
114
+ if (this._transport) {
115
+ try {
116
+ await this._transport.close();
117
+ } catch (error) {
118
+ console.error('Error closing MCP server transport:', error);
119
+ }
120
+ this._transport = null;
121
+ }
122
+ }
123
+
124
+ async listTools(): Promise<MCPTool[]> {
125
+ if (!this._session) {
126
+ throw new Error('Server not initialized. Call connect() first.');
127
+ }
128
+
129
+ if (
130
+ this.cacheToolsList &&
131
+ !this._cacheDirty &&
132
+ this._toolsList.length > 0
133
+ ) {
134
+ return this._toolsList;
135
+ }
136
+
137
+ try {
138
+ const { ListToolsResultSchema } = await import(
139
+ '@modelcontextprotocol/sdk/types.js'
140
+ );
141
+
142
+ const response = await this._session.listTools();
143
+
144
+ const parsedResponse = ListToolsResultSchema.parse(response);
145
+
146
+ // Map to openai/agents MCPTool type
147
+ this._toolsList = parsedResponse.tools.map((tool: any) => ({
148
+ name: tool.name,
149
+ description: tool.description,
150
+ inputSchema: {
151
+ type: 'object' as const,
152
+ properties: tool.inputSchema?.properties || {},
153
+ required: tool.inputSchema?.required || [],
154
+ additionalProperties: tool.inputSchema?.additionalProperties ?? false
155
+ }
156
+ }));
157
+
158
+ this._cacheDirty = false;
159
+
160
+ return this._toolsList;
161
+ } catch (error) {
162
+ console.error(`Error listing tools from ${this.name}:`, error);
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ async callTool(
168
+ toolName: string,
169
+ args: Record<string, unknown> | null
170
+ ): Promise<CallToolResultContent> {
171
+ if (!this._session) {
172
+ throw new Error('Server not initialized. Call connect() first.');
173
+ }
174
+
175
+ try {
176
+ const { CallToolResultSchema } = await import(
177
+ '@modelcontextprotocol/sdk/types.js'
178
+ );
179
+
180
+ const response = await this._session.callTool(
181
+ {
182
+ name: toolName,
183
+ arguments: args ?? {}
184
+ },
185
+ undefined,
186
+ {
187
+ timeout: this._options.timeout ?? 30000
188
+ }
189
+ );
190
+
191
+ // Parse and validate using MCP SDK schema
192
+ const parsed = CallToolResultSchema.parse(response);
193
+ const result = parsed.content;
194
+
195
+ // Return the content array as expected by openai/agents
196
+ // CallToolResultContent is { type: string; text: string }[]
197
+ return result as CallToolResultContent;
198
+ } catch (error) {
199
+ console.error(`Error calling tool ${toolName}:`, error);
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ async invalidateToolsCache(): Promise<void> {
205
+ this._cacheDirty = true;
206
+ }
207
+
208
+ private _session: any | null = null;
209
+ private _toolsList: MCPTool[] = [];
210
+ private _cacheDirty = true;
211
+ private _transport: any = null;
212
+ private _options: MCPServerStreamableHttpOptions;
213
+ }
@@ -0,0 +1,409 @@
1
+ import { VDomModel } from '@jupyterlab/ui-components';
2
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
3
+
4
+ const PLUGIN_ID = '@jupyterlite/ai:settings-model';
5
+
6
+ export interface IProviderConfig {
7
+ id: string;
8
+ name: string;
9
+ provider: 'anthropic' | 'mistral' | 'ollama';
10
+ model: string;
11
+ apiKey?: string;
12
+ baseURL?: string;
13
+ headers?: Record<string, string>;
14
+ customSettings?: Record<string, any>;
15
+ [key: string]: any; // Index signature for JupyterLab settings compatibility
16
+ }
17
+
18
+ export interface IMCPServerConfig {
19
+ id: string;
20
+ name: string;
21
+ url: string;
22
+ enabled: boolean;
23
+ [key: string]: any; // Index signature for JupyterLab settings compatibility
24
+ }
25
+
26
+ export interface IAIConfig {
27
+ // List of configured providers
28
+ providers: IProviderConfig[];
29
+ // Active provider IDs for different use cases
30
+ activeProvider: string; // Provider for chat
31
+ activeCompleterProvider?: string; // Provider for completions (if different)
32
+ // When true, use the same provider for chat and completions
33
+ useSameProviderForChatAndCompleter: boolean;
34
+ // MCP servers configuration
35
+ mcpServers: IMCPServerConfig[];
36
+ // Global settings
37
+ temperature: number;
38
+ maxTokens?: number;
39
+ contextAwareness: boolean;
40
+ codeExecution: boolean;
41
+ systemPrompt: string;
42
+ toolsEnabled: boolean;
43
+ // Chat behavior settings
44
+ sendWithShiftEnter: boolean;
45
+ // Maximum number of turns/iterations when using tools
46
+ maxTurns: number;
47
+ // Token usage display setting
48
+ showTokenUsage: boolean;
49
+ // Commands that require approval before execution
50
+ commandsRequiringApproval: string[];
51
+ }
52
+
53
+ export class AISettingsModel extends VDomModel {
54
+ private _config: IAIConfig = {
55
+ providers: [],
56
+ activeProvider: '',
57
+ activeCompleterProvider: undefined,
58
+ useSameProviderForChatAndCompleter: true,
59
+ mcpServers: [],
60
+ temperature: 0.7,
61
+ maxTokens: undefined,
62
+ contextAwareness: true,
63
+ codeExecution: false,
64
+ toolsEnabled: true,
65
+ sendWithShiftEnter: false,
66
+ maxTurns: 25,
67
+ showTokenUsage: false,
68
+ commandsRequiringApproval: [
69
+ 'notebook:restart-run-all',
70
+ 'notebook:run-cell',
71
+ 'notebook:run-cell-and-select-next',
72
+ 'notebook:run-cell-and-insert-below',
73
+ 'notebook:run-all-cells',
74
+ 'notebook:run-all-above',
75
+ 'notebook:run-all-below',
76
+ 'console:execute',
77
+ 'console:execute-forced',
78
+ 'fileeditor:run-code',
79
+ 'kernelmenu:run',
80
+ 'kernelmenu:restart-and-run-all',
81
+ 'runmenu:run-all'
82
+ ],
83
+ systemPrompt: `You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.
84
+
85
+ ## Your Core Mission
86
+ You're designed to be a capable partner for data science, research, and development work in Jupyter notebooks. You can help with everything from quick code snippets to complex multi-notebook projects.
87
+
88
+ ## Your Capabilities
89
+ **📁 File & Project Management:**
90
+ - Create, read, edit, and organize Python files and notebooks
91
+ - Manage project structure and navigate file systems
92
+ - Help with version control and project organization
93
+
94
+ **📊 Notebook Operations:**
95
+ - Create new notebooks and manage existing ones
96
+ - Add, edit, delete, and run cells (both code and markdown)
97
+ - Help with notebook structure and organization
98
+ - Retrieve and analyze cell outputs and execution results
99
+
100
+ **🧠 Coding & Development:**
101
+ - Write, debug, and optimize Python code
102
+ - Explain complex algorithms and data structures
103
+ - Help with data analysis, visualization, and machine learning
104
+ - Support for scientific computing libraries (numpy, pandas, matplotlib, etc.)
105
+ - Code reviews and best practices recommendations
106
+
107
+ **💡 Adaptive Assistance:**
108
+ - Understand context from your current work environment
109
+ - Provide suggestions tailored to your specific use case
110
+ - Help with both quick fixes and long-term project planning
111
+
112
+ ## How I Work
113
+ I can actively interact with your JupyterLab environment using specialized tools. When you ask me to perform actions, I can:
114
+ - Execute operations directly in your notebooks
115
+ - Create and modify files as needed
116
+ - Run code and analyze results
117
+ - Make systematic changes across multiple files
118
+
119
+ ## My Approach
120
+ - **Context-aware**: I understand you're working in a data science/research environment
121
+ - **Practical**: I focus on actionable solutions that work in your current setup
122
+ - **Educational**: I explain my reasoning and teach best practices along the way
123
+ - **Collaborative**: Think of me as a pair programming partner, not just a code generator
124
+
125
+ ## Communication Style & Agent Behavior
126
+ - **Conversational**: I maintain a friendly, natural conversation flow throughout our interaction
127
+ - **Progress Updates**: I write brief progress messages between tool uses that appear directly in our conversation
128
+ - **No Filler**: I avoid empty acknowledgments like "Sounds good!" or "Okay, I will..." - I get straight to work
129
+ - **Purposeful Communication**: I start with what I'm doing, use tools, then share what I found and what's next
130
+ - **Active Narration**: I actively write progress updates like "Looking at the current code structure..." or "Found the issue in the notebook..." between tool calls
131
+ - **Checkpoint Updates**: After several operations, I summarize what I've accomplished and what remains
132
+ - **Natural Flow**: My explanations and progress reports appear as normal conversation text, not just in tool blocks
133
+
134
+ ## IMPORTANT: Always write progress messages between tools that explain what you're doing and what you found. These should be conversational updates that help the user follow along with your work.
135
+
136
+ ## Technical Communication
137
+ - Code is formatted in proper markdown blocks with syntax highlighting
138
+ - Mathematical notation uses LaTeX formatting: \\(equations\\) and \\[display math\\]
139
+ - I provide context for my actions and explain my reasoning as I work
140
+ - When creating or modifying multiple files, I give brief summaries of changes
141
+ - I keep users informed of progress while staying focused on the task
142
+
143
+ ## Multi-Step Task Handling
144
+ When users request complex tasks that require multiple steps (like "create a notebook with example cells"), I use tools in sequence to accomplish the complete task. For example:
145
+ - First use create_notebook to create the notebook
146
+ - Then use add_code_cell or add_markdown_cell to add cells
147
+ - Use set_cell_content to add content to cells as needed
148
+ - Use run_cell to execute code when appropriate
149
+
150
+ Always think through multi-step tasks and use tools to fully complete the user's request rather than stopping after just one action.
151
+
152
+ Ready to help you build something great! What are you working on?`
153
+ };
154
+
155
+ private _settingRegistry: ISettingRegistry;
156
+ private _settings: ISettingRegistry.ISettings | null = null;
157
+
158
+ constructor(options: AISettingsModel.IOptions) {
159
+ super();
160
+ this._settingRegistry = options.settingRegistry;
161
+ this.initializeSettings();
162
+ }
163
+
164
+ private async initializeSettings(): Promise<void> {
165
+ try {
166
+ this._settings = await this._settingRegistry.load(PLUGIN_ID);
167
+ this.loadFromSettings();
168
+
169
+ // Listen for settings changes
170
+ this._settings.changed.connect(this.onSettingsChanged, this);
171
+
172
+ this.stateChanged.emit(void 0);
173
+ } catch (error) {
174
+ console.warn('Failed to load JupyterLab settings:', error);
175
+ this.stateChanged.emit(void 0);
176
+ }
177
+ }
178
+
179
+ private onSettingsChanged(): void {
180
+ this.loadFromSettings();
181
+ this.stateChanged.emit(void 0);
182
+ }
183
+
184
+ private loadFromSettings(): void {
185
+ if (!this._settings) {
186
+ return;
187
+ }
188
+
189
+ // Merge JupyterLab settings with defaults
190
+ const settingsData = this._settings.composite as Partial<IAIConfig>;
191
+ this._config = {
192
+ ...this._config,
193
+ ...settingsData
194
+ };
195
+ }
196
+
197
+ get config(): IAIConfig {
198
+ return { ...this._config };
199
+ }
200
+
201
+ get providers(): IProviderConfig[] {
202
+ return [...this._config.providers];
203
+ }
204
+
205
+ getProvider(id: string): IProviderConfig | undefined {
206
+ return this._config.providers.find(p => p.id === id);
207
+ }
208
+
209
+ getActiveProvider(): IProviderConfig | undefined {
210
+ return this.getProvider(this._config.activeProvider);
211
+ }
212
+
213
+ getCompleterProvider(): IProviderConfig | undefined {
214
+ if (this._config.useSameProviderForChatAndCompleter) {
215
+ return this.getActiveProvider();
216
+ }
217
+ return this._config.activeCompleterProvider
218
+ ? this.getProvider(this._config.activeCompleterProvider)
219
+ : this.getActiveProvider();
220
+ }
221
+
222
+ async addProvider(
223
+ providerConfig: Omit<IProviderConfig, 'id'>
224
+ ): Promise<string> {
225
+ const id = `${providerConfig.provider}-${Date.now()}`;
226
+ const newProvider: IProviderConfig = {
227
+ id,
228
+ name: providerConfig.name,
229
+ provider: providerConfig.provider,
230
+ model: providerConfig.model,
231
+ apiKey: providerConfig.apiKey,
232
+ baseURL: providerConfig.baseURL,
233
+ headers: providerConfig.headers,
234
+ customSettings: providerConfig.customSettings
235
+ };
236
+
237
+ this._config.providers.push(newProvider);
238
+
239
+ // If this is the first provider, make it active
240
+ if (this._config.providers.length === 1) {
241
+ this._config.activeProvider = id;
242
+ // Save both providers and activeProvider
243
+ await this.saveSetting('providers', this._config.providers);
244
+ await this.saveSetting('activeProvider', this._config.activeProvider);
245
+ } else {
246
+ // Only save providers
247
+ await this.saveSetting('providers', this._config.providers);
248
+ }
249
+
250
+ return id;
251
+ }
252
+
253
+ async removeProvider(id: string): Promise<void> {
254
+ const index = this._config.providers.findIndex(p => p.id === id);
255
+ if (index === -1) {
256
+ return;
257
+ }
258
+
259
+ this._config.providers.splice(index, 1);
260
+ await this.saveSetting('providers', this._config.providers);
261
+
262
+ // If this was the active provider, select a new one
263
+ if (this._config.activeProvider === id) {
264
+ this._config.activeProvider =
265
+ this._config.providers.length > 0 ? this._config.providers[0].id : '';
266
+ await this.saveSetting('activeProvider', this._config.activeProvider);
267
+ }
268
+
269
+ if (this._config.activeCompleterProvider === id) {
270
+ this._config.activeCompleterProvider = undefined;
271
+ await this.saveSetting(
272
+ 'activeCompleterProvider',
273
+ this._config.activeCompleterProvider
274
+ );
275
+ }
276
+ }
277
+
278
+ async updateProvider(
279
+ id: string,
280
+ updates: Partial<IProviderConfig>
281
+ ): Promise<void> {
282
+ const provider = this.getProvider(id);
283
+ if (!provider) {
284
+ return;
285
+ }
286
+
287
+ Object.assign(provider, updates);
288
+ await this.saveSetting('providers', this._config.providers);
289
+ }
290
+
291
+ async setActiveProvider(id: string): Promise<void> {
292
+ if (this.getProvider(id)) {
293
+ this._config.activeProvider = id;
294
+ await this.saveSetting('activeProvider', this._config.activeProvider);
295
+ }
296
+ }
297
+
298
+ async setActiveCompleterProvider(id: string | undefined): Promise<void> {
299
+ this._config.activeCompleterProvider = id;
300
+ await this.saveSetting(
301
+ 'activeCompleterProvider',
302
+ this._config.activeCompleterProvider
303
+ );
304
+ }
305
+
306
+ get mcpServers(): IMCPServerConfig[] {
307
+ return [...this._config.mcpServers];
308
+ }
309
+
310
+ getMCPServer(id: string): IMCPServerConfig | undefined {
311
+ return this._config.mcpServers.find(s => s.id === id);
312
+ }
313
+
314
+ async addMCPServer(
315
+ serverConfig: Omit<IMCPServerConfig, 'id'>
316
+ ): Promise<string> {
317
+ const id = `mcp-${Date.now()}`;
318
+ const newServer: IMCPServerConfig = {
319
+ id,
320
+ name: serverConfig.name,
321
+ url: serverConfig.url,
322
+ enabled: serverConfig.enabled
323
+ };
324
+
325
+ this._config.mcpServers.push(newServer);
326
+ await this.saveSetting('mcpServers', this._config.mcpServers);
327
+ return id;
328
+ }
329
+
330
+ async removeMCPServer(id: string): Promise<void> {
331
+ const index = this._config.mcpServers.findIndex(s => s.id === id);
332
+ if (index === -1) {
333
+ return;
334
+ }
335
+
336
+ this._config.mcpServers.splice(index, 1);
337
+ await this.saveSetting('mcpServers', this._config.mcpServers);
338
+ }
339
+
340
+ async updateMCPServer(
341
+ id: string,
342
+ updates: Partial<IMCPServerConfig>
343
+ ): Promise<void> {
344
+ const server = this.getMCPServer(id);
345
+ if (!server) {
346
+ return;
347
+ }
348
+
349
+ Object.assign(server, updates);
350
+ await this.saveSetting('mcpServers', this._config.mcpServers);
351
+ }
352
+
353
+ async updateConfig(updates: Partial<IAIConfig>): Promise<void> {
354
+ // Update config and save only changed settings
355
+ const promises: Promise<void>[] = [];
356
+
357
+ for (const [key, value] of Object.entries(updates)) {
358
+ if (
359
+ key in this._config &&
360
+ this._config[key as keyof IAIConfig] !== value
361
+ ) {
362
+ (this._config as any)[key] = value;
363
+ promises.push(this.saveSetting(key as keyof IAIConfig, value));
364
+ }
365
+ }
366
+
367
+ // Wait for all settings to be saved
368
+ await Promise.all(promises);
369
+ }
370
+
371
+ getApiKey(id: string): string {
372
+ // First check the active completer provider
373
+ const activeCompleterProvider = this.getCompleterProvider();
374
+ if (activeCompleterProvider && activeCompleterProvider.id === id) {
375
+ return activeCompleterProvider.apiKey || '';
376
+ }
377
+
378
+ // Fallback to active chat provider
379
+ const activeProvider = this.getActiveProvider();
380
+ if (activeProvider && activeProvider.id === id) {
381
+ return activeProvider.apiKey || '';
382
+ }
383
+
384
+ return '';
385
+ }
386
+
387
+ private async saveSetting(key: keyof IAIConfig, value: any): Promise<void> {
388
+ try {
389
+ if (this._settings) {
390
+ // Only save the specific setting that changed
391
+ if (value !== undefined) {
392
+ await this._settings.set(key, value as any);
393
+ }
394
+ }
395
+ } catch (error) {
396
+ console.warn(
397
+ `Failed to save setting '${key}' to JupyterLab settings, falling back to localStorage:`,
398
+ error
399
+ );
400
+ }
401
+ this.stateChanged.emit(void 0);
402
+ }
403
+ }
404
+
405
+ export namespace AISettingsModel {
406
+ export interface IOptions {
407
+ settingRegistry: ISettingRegistry;
408
+ }
409
+ }