@jupyterlite/ai 0.8.1 → 0.9.0-a1

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