@jupyterlite/ai 0.9.0-a3 → 0.9.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 (50) hide show
  1. package/README.md +20 -89
  2. package/lib/agent.d.ts +10 -4
  3. package/lib/agent.js +30 -17
  4. package/lib/chat-model.d.ts +6 -0
  5. package/lib/chat-model.js +144 -17
  6. package/lib/completion/completion-provider.js +1 -13
  7. package/lib/components/completion-status.d.ts +20 -0
  8. package/lib/components/completion-status.js +51 -0
  9. package/lib/components/index.d.ts +1 -0
  10. package/lib/components/index.js +1 -0
  11. package/lib/components/model-select.js +1 -2
  12. package/lib/diff-manager.d.ts +25 -0
  13. package/lib/diff-manager.js +60 -0
  14. package/lib/icons.d.ts +0 -1
  15. package/lib/icons.js +2 -6
  16. package/lib/index.d.ts +2 -2
  17. package/lib/index.js +54 -23
  18. package/lib/models/settings-model.d.ts +4 -0
  19. package/lib/models/settings-model.js +24 -2
  20. package/lib/providers/built-in-providers.d.ts +0 -4
  21. package/lib/providers/built-in-providers.js +17 -23
  22. package/lib/tokens.d.ts +74 -0
  23. package/lib/tokens.js +4 -0
  24. package/lib/tools/commands.js +36 -35
  25. package/lib/tools/file.d.ts +10 -1
  26. package/lib/tools/file.js +235 -146
  27. package/lib/tools/notebook.d.ts +2 -3
  28. package/lib/tools/notebook.js +11 -11
  29. package/lib/widgets/ai-settings.js +78 -13
  30. package/lib/widgets/provider-config-dialog.js +15 -8
  31. package/package.json +5 -3
  32. package/schema/settings-model.json +25 -0
  33. package/src/agent.ts +35 -20
  34. package/src/chat-model.ts +182 -19
  35. package/src/completion/completion-provider.ts +1 -14
  36. package/src/components/completion-status.tsx +79 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/components/model-select.tsx +0 -3
  39. package/src/diff-manager.ts +81 -0
  40. package/src/icons.ts +2 -7
  41. package/src/index.ts +74 -24
  42. package/src/models/settings-model.ts +28 -2
  43. package/src/providers/built-in-providers.ts +17 -24
  44. package/src/tokens.ts +78 -0
  45. package/src/tools/commands.ts +45 -40
  46. package/src/tools/file.ts +295 -164
  47. package/src/tools/notebook.ts +13 -14
  48. package/src/widgets/ai-settings.tsx +184 -35
  49. package/src/widgets/provider-config-dialog.tsx +43 -16
  50. package/style/base.css +14 -0
package/README.md CHANGED
@@ -52,13 +52,25 @@ The process is different for each provider, so you may refer to their documentat
52
52
 
53
53
  ![screenshot showing the dialog to add a new provider](https://github.com/user-attachments/assets/823c71c6-5807-44c8-80b6-2e59379a65d5)
54
54
 
55
+ ### Using a generic OpenAI-compatible provider
56
+
57
+ The Generic provider allows you to connect to any OpenAI-compatible API endpoint, including local servers like Ollama and LiteLLM.
58
+
59
+ 1. In JupyterLab, open the AI settings panel and go to the **Providers** section
60
+ 2. Click on "Add a new provider"
61
+ 3. Select the **Generic (OpenAI-compatible)** provider
62
+ 4. Configure the following settings:
63
+ - **Base URL**: The base URL of your API endpoint (suggestions are provided for common local servers)
64
+ - **Model**: The model name to use
65
+ - **API Key**: Your API key (if required by the provider)
66
+
55
67
  ### Using Ollama
56
68
 
57
69
  [Ollama](https://ollama.com/) allows you to run open-weight LLMs locally on your machine.
58
70
 
59
71
  #### Setting up Ollama
60
72
 
61
- 1. Install Ollama following the instructions at https://ollama.com/download
73
+ 1. Install Ollama following the instructions at <https://ollama.com/download>
62
74
  2. Pull a model, for example:
63
75
 
64
76
  ```bash
@@ -71,21 +83,11 @@ ollama pull llama3.2
71
83
 
72
84
  1. In JupyterLab, open the AI settings panel and go to the **Providers** section
73
85
  2. Click on "Add a new provider"
74
- 3. Select the **Ollama** provider
86
+ 3. Select the **Generic (OpenAI-compatible)** provider
75
87
  4. Configure the following settings:
88
+ - **Base URL**: Select `http://localhost:11434/v1` from the suggestions (or enter manually)
76
89
  - **Model**: The model name you pulled (e.g., `llama3.2`)
77
-
78
- ### Using a generic OpenAI-compatible provider
79
-
80
- The Generic provider allows you to connect to any OpenAI-compatible API endpoint.
81
-
82
- 1. In JupyterLab, open the AI settings panel and go to the **Providers** section
83
- 2. Click on "Add a new provider"
84
- 3. Select the **Generic** provider
85
- 4. Configure the following settings:
86
- - **Base URL**: The base URL of your API endpoint
87
- - **Model**: The model name to use
88
- - **API Key**: Your API key (if required by the provider)
90
+ - **API Key**: Leave empty (not required for Ollama)
89
91
 
90
92
  ### Using LiteLLM Proxy
91
93
 
@@ -97,7 +99,7 @@ Using LiteLLM Proxy with jupyterlite-ai provides flexibility to switch between d
97
99
 
98
100
  1. Install LiteLLM:
99
101
 
100
- Follow the instructions at https://docs.litellm.ai/docs/simple_proxy.
102
+ Follow the instructions at <https://docs.litellm.ai/docs/simple_proxy>.
101
103
 
102
104
  2. Create a `litellm_config.yaml` file with your model configuration:
103
105
 
@@ -144,7 +146,7 @@ Providers are based on the [Vercel AI SDK](https://sdk.vercel.ai/docs/introducti
144
146
 
145
147
  ### Registering a Custom Provider
146
148
 
147
- **Example: Registering a custom OpenAI-compatible provider**
149
+ #### Example: Registering a custom OpenAI-compatible provider
148
150
 
149
151
  ```typescript
150
152
  import {
@@ -192,7 +194,7 @@ The provider configuration object requires the following properties:
192
194
  - `supportsBaseURL`: Whether the provider supports a custom base URL
193
195
  - `factory`: Function that creates and returns a language model (the registry automatically wraps it for chat usage)
194
196
 
195
- **Example: Using a custom fetch function**
197
+ #### Example: Using a custom fetch function
196
198
 
197
199
  You can provide a custom `fetch` function to the provider, which is useful for adding custom headers, handling authentication, or routing requests through a proxy:
198
200
 
@@ -253,75 +255,4 @@ pip uninstall jupyterlite-ai
253
255
 
254
256
  ## Contributing
255
257
 
256
- ### Development install
257
-
258
- Note: You will need NodeJS to build the extension package.
259
-
260
- The `jlpm` command is JupyterLab's pinned version of
261
- [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
262
- `yarn` or `npm` in lieu of `jlpm` below.
263
-
264
- ```bash
265
- # Clone the repo to your local environment
266
- # Change directory to the jupyterlite_ai directory
267
- # Install package in development mode
268
- pip install -e "."
269
- # Link your development version of the extension with JupyterLab
270
- jupyter labextension develop . --overwrite
271
- # Rebuild extension Typescript source after making changes
272
- jlpm build
273
- ```
274
-
275
- You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
276
-
277
- ```bash
278
- # Watch the source directory in one terminal, automatically rebuilding when needed
279
- jlpm watch
280
- # Run JupyterLab in another terminal
281
- jupyter lab
282
- ```
283
-
284
- With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
285
-
286
- By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
287
-
288
- ```bash
289
- jupyter lab build --minimize=False
290
- ```
291
-
292
- ### Running UI tests
293
-
294
- The UI tests use Playwright and can be configured with environment variables:
295
-
296
- - `PWVIDEO`: Controls video recording during tests (default: `retain-on-failure`)
297
- - `on`: Record video for all tests
298
- - `off`: Do not record video
299
- - `retain-on-failure`: Only keep videos for failed tests
300
- - `PWSLOWMO`: Adds a delay (in milliseconds) between Playwright actions for debugging (default: `0`)
301
-
302
- Example usage:
303
-
304
- ```bash
305
- # Record all test videos
306
- PWVIDEO=on jlpm playwright test
307
-
308
- # Slow down test execution by 500ms per action
309
- PWSLOWMO=500 jlpm playwright test
310
-
311
- # Combine both options
312
- PWVIDEO=on PWSLOWMO=1000 jlpm playwright test
313
- ```
314
-
315
- ### Development uninstall
316
-
317
- ```bash
318
- pip uninstall jupyterlite-ai
319
- ```
320
-
321
- In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
322
- command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
323
- folder is located. Then you can remove the symlink named `@jupyterlite/ai` within that folder.
324
-
325
- ### Packaging the extension
326
-
327
- See [RELEASE](RELEASE.md)
258
+ See [CONTRIBUTING](CONTRIBUTING.md)
package/lib/agent.d.ts CHANGED
@@ -73,18 +73,18 @@ export interface IAgentEventTypeMap {
73
73
  tool_call_start: {
74
74
  callId: string;
75
75
  toolName: string;
76
- input: any;
76
+ input: string;
77
77
  };
78
78
  tool_call_complete: {
79
79
  callId: string;
80
80
  toolName: string;
81
- output: any;
81
+ output: string;
82
82
  isError: boolean;
83
83
  };
84
84
  tool_approval_required: {
85
85
  interruptionId: string;
86
86
  toolName: string;
87
- toolInput: any;
87
+ toolInput: string;
88
88
  callId?: string;
89
89
  };
90
90
  grouped_approval_required: {
@@ -92,7 +92,7 @@ export interface IAgentEventTypeMap {
92
92
  approvals: Array<{
93
93
  interruptionId: string;
94
94
  toolName: string;
95
- toolInput: any;
95
+ toolInput: string;
96
96
  }>;
97
97
  };
98
98
  error: {
@@ -232,6 +232,12 @@ export declare class AgentManager {
232
232
  * @param result The async iterable result from agent execution
233
233
  */
234
234
  private _processRunResult;
235
+ /**
236
+ * Formats tool input for display by pretty-printing JSON strings.
237
+ * @param input The tool input string to format
238
+ * @returns Pretty-printed JSON string
239
+ */
240
+ private _formatToolInput;
235
241
  /**
236
242
  * Handles the start of a tool call from the model event.
237
243
  * @param modelEvent The model event containing tool call information
package/lib/agent.js CHANGED
@@ -504,6 +504,22 @@ export class AgentManager {
504
504
  }
505
505
  }
506
506
  }
507
+ /**
508
+ * Formats tool input for display by pretty-printing JSON strings.
509
+ * @param input The tool input string to format
510
+ * @returns Pretty-printed JSON string
511
+ */
512
+ _formatToolInput(input) {
513
+ try {
514
+ // Parse and re-stringify with formatting
515
+ const parsed = JSON.parse(input);
516
+ return JSON.stringify(parsed, null, 2);
517
+ }
518
+ catch {
519
+ // If parsing fails, return the string as-is
520
+ return input;
521
+ }
522
+ }
507
523
  /**
508
524
  * Handles the start of a tool call from the model event.
509
525
  * @param modelEvent The model event containing tool call information
@@ -512,19 +528,12 @@ export class AgentManager {
512
528
  const toolCallId = modelEvent.toolCallId;
513
529
  const toolName = modelEvent.toolName;
514
530
  const toolInput = modelEvent.input;
515
- let parsedToolInput;
516
- try {
517
- parsedToolInput = JSON.parse(toolInput);
518
- }
519
- catch (error) {
520
- parsedToolInput = {};
521
- }
522
531
  this._agentEvent.emit({
523
532
  type: 'tool_call_start',
524
533
  data: {
525
534
  callId: toolCallId,
526
535
  toolName,
527
- input: parsedToolInput
536
+ input: this._formatToolInput(toolInput)
528
537
  }
529
538
  });
530
539
  }
@@ -540,7 +549,7 @@ export class AgentManager {
540
549
  ? toolCallOutput.output
541
550
  : JSON.stringify(toolCallOutput.output, null, 2);
542
551
  const isError = toolCallOutput.rawItem.type === 'function_call_result' &&
543
- toolCallOutput.rawItem.error;
552
+ toolCallOutput.rawItem.status === 'incomplete';
544
553
  const toolName = toolCallOutput.rawItem.type === 'function_call_result'
545
554
  ? toolCallOutput.rawItem.name
546
555
  : 'Unknown Tool';
@@ -559,17 +568,19 @@ export class AgentManager {
559
568
  * @param interruption The tool approval interruption item
560
569
  */
561
570
  async _handleSingleToolApproval(interruption) {
562
- const toolName = interruption.rawItem?.name || 'Unknown Tool';
563
- const toolInput = interruption.rawItem?.arguments || {};
571
+ const toolName = interruption.rawItem.name || 'Unknown Tool';
572
+ const toolInput = interruption.rawItem.arguments || '{}';
564
573
  const interruptionId = `int-${Date.now()}-${Math.random()}`;
565
- const callId = interruption.rawItem?.callId;
574
+ const callId = interruption.rawItem.type === 'function_call'
575
+ ? interruption.rawItem.callId
576
+ : undefined;
566
577
  this._pendingApprovals.set(interruptionId, { interruption });
567
578
  this._agentEvent.emit({
568
579
  type: 'tool_approval_required',
569
580
  data: {
570
581
  interruptionId,
571
582
  toolName,
572
- toolInput,
583
+ toolInput: this._formatToolInput(toolInput),
573
584
  callId
574
585
  }
575
586
  });
@@ -581,14 +592,14 @@ export class AgentManager {
581
592
  async _handleGroupedToolApprovals(interruptions) {
582
593
  const groupId = `group-${Date.now()}-${Math.random()}`;
583
594
  const approvals = interruptions.map(interruption => {
584
- const toolName = interruption.rawItem?.name || 'Unknown Tool';
585
- const toolInput = interruption.rawItem?.arguments || {};
595
+ const toolName = interruption.rawItem.name || 'Unknown Tool';
596
+ const toolInput = interruption.rawItem.arguments || '{}';
586
597
  const interruptionId = `int-${Date.now()}-${Math.random()}`;
587
598
  this._pendingApprovals.set(interruptionId, { interruption, groupId });
588
599
  return {
589
600
  interruptionId,
590
601
  toolName,
591
- toolInput
602
+ toolInput: this._formatToolInput(toolInput)
592
603
  };
593
604
  });
594
605
  this._agentEvent.emit({
@@ -668,7 +679,9 @@ Guidelines:
668
679
  - Use natural, conversational tone throughout
669
680
 
670
681
  COMMAND DISCOVERY:
671
- - When you want to execute JupyterLab commands, ALWAYS use the 'discover_commands' tool first to find available commands and their metadata.
682
+ - When you want to execute JupyterLab commands, ALWAYS use the 'discover_commands' tool first to find available commands and their metadata, with the optional query parameter.
683
+ - The query should typically be a single word, e.g., 'terminal', 'notebook', 'cell', 'file', 'edit', 'view', 'run', etc, to find relevant commands.
684
+ - If searching with a query does not yield the desired command, try again with a different query or use an empty query to list all commands.
672
685
  - This ensures you have complete information about command IDs, descriptions, and required arguments before attempting to execute them. Only after discovering the available commands should you use the 'execute_command' tool with the correct command ID and arguments.
673
686
 
674
687
  TOOL SELECTION GUIDELINES:
@@ -138,6 +138,12 @@ export declare class AIChatModel extends AbstractChatModel {
138
138
  * @returns Array of formatted attachment contents
139
139
  */
140
140
  private _processAttachments;
141
+ /**
142
+ * Reads the content of a notebook cell.
143
+ * @param attachment The notebook attachment to read
144
+ * @returns Cell content as string or null if unable to read
145
+ */
146
+ private _readNotebookCells;
141
147
  /**
142
148
  * Reads the content of a file attachment.
143
149
  * @param attachment The file attachment to read
package/lib/chat-model.js CHANGED
@@ -106,7 +106,7 @@ export class AIChatModel extends AbstractChatModel {
106
106
  time: Date.now() / 1000,
107
107
  type: 'msg',
108
108
  raw_time: false,
109
- attachments: this.input.attachments
109
+ attachments: [...this.input.attachments]
110
110
  };
111
111
  this.messageAdded(userMessage);
112
112
  // Check if we have valid configuration
@@ -127,6 +127,8 @@ export class AIChatModel extends AbstractChatModel {
127
127
  let enhancedMessage = message.body;
128
128
  if (this.input.attachments.length > 0) {
129
129
  const attachmentContents = await this._processAttachments(this.input.attachments);
130
+ // Clear attachments right after processing
131
+ this.input.clearAttachments();
130
132
  if (attachmentContents.length > 0) {
131
133
  enhancedMessage +=
132
134
  '\n\n--- Attached Files ---\n' + attachmentContents.join('\n\n');
@@ -134,8 +136,6 @@ export class AIChatModel extends AbstractChatModel {
134
136
  }
135
137
  this.updateWriters([{ user: this._getAIUser() }]);
136
138
  await this._agentManager.generateResponse(enhancedMessage);
137
- // Clear attachments after processing
138
- this.input.clearAttachments();
139
139
  }
140
140
  catch (error) {
141
141
  const errorMessage = {
@@ -219,10 +219,7 @@ export class AIChatModel extends AbstractChatModel {
219
219
  */
220
220
  _onSettingsChanged() {
221
221
  const config = this._settingsModel.config;
222
- this.config = {
223
- ...config,
224
- enableCodeToolbar: true
225
- };
222
+ this.config = { ...config, enableCodeToolbar: true };
226
223
  // Agent manager handles agent recreation automatically via its own settings listener
227
224
  }
228
225
  /**
@@ -312,7 +309,7 @@ export class AIChatModel extends AbstractChatModel {
312
309
  <div class="jp-ai-tool-body">
313
310
  <div class="jp-ai-tool-section">
314
311
  <div class="jp-ai-tool-label">Input</div>
315
- <pre class="jp-ai-tool-code"><code>${JSON.stringify(event.data.input, null, 2)}</code></pre>
312
+ <pre class="jp-ai-tool-code"><code>${event.data.input}</code></pre>
316
313
  </div>
317
314
  </div>
318
315
  </details>`,
@@ -394,7 +391,7 @@ export class AIChatModel extends AbstractChatModel {
394
391
  <div class="jp-ai-tool-body">
395
392
  <div class="jp-ai-tool-section">
396
393
  <div class="jp-ai-tool-label">${assistantName} wants to execute this tool. Do you approve?</div>
397
- <pre class="jp-ai-tool-code"><code>${JSON.stringify(event.data.toolInput, null, 2)}</code></pre>
394
+ <pre class="jp-ai-tool-code"><code>${event.data.toolInput}</code></pre>
398
395
  </div>
399
396
  [APPROVAL_BUTTONS:${event.data.interruptionId}]
400
397
  </div>
@@ -414,7 +411,9 @@ export class AIChatModel extends AbstractChatModel {
414
411
 
415
412
  ${assistantName} wants to execute this tool. Do you approve?
416
413
 
417
- ${JSON.stringify(event.data.toolInput, null, 2)}
414
+ \`\`\`json
415
+ ${event.data.toolInput}
416
+ \`\`\`
418
417
 
419
418
  [APPROVAL_BUTTONS:${event.data.interruptionId}]`,
420
419
  sender: this._getAIUser(),
@@ -434,7 +433,7 @@ ${JSON.stringify(event.data.toolInput, null, 2)}
434
433
  const assistantName = this._getAIUser().display_name;
435
434
  const approvalMessageId = UUID.uuid4();
436
435
  const toolsList = event.data.approvals
437
- .map((info, index) => `**${index + 1}. ${info.toolName}**\n${JSON.stringify(info.toolInput, null, 2)}\n`)
436
+ .map((info, index) => `**${index + 1}. ${info.toolName}**\n\`\`\`json\n${info.toolInput}\n\`\`\`\n`)
438
437
  .join('\n\n');
439
438
  const approvalMessage = {
440
439
  body: `**🤖 Multiple Tool Approvals Required**
@@ -477,12 +476,19 @@ ${toolsList}
477
476
  const contents = [];
478
477
  for (const attachment of attachments) {
479
478
  try {
480
- const fileContent = await this._readFileAttachment(attachment);
481
- if (fileContent) {
482
- // Get file extension for syntax highlighting
483
- const fileExtension = PathExt.extname(attachment.value).toLowerCase();
484
- const language = fileExtension === '.ipynb' ? 'json' : '';
485
- contents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
479
+ if (attachment.type === 'notebook' && attachment.cells?.length) {
480
+ const cellContents = await this._readNotebookCells(attachment);
481
+ if (cellContents) {
482
+ contents.push(cellContents);
483
+ }
484
+ }
485
+ else {
486
+ const fileContent = await this._readFileAttachment(attachment);
487
+ if (fileContent) {
488
+ const fileExtension = PathExt.extname(attachment.value).toLowerCase();
489
+ const language = fileExtension === '.ipynb' ? 'json' : '';
490
+ contents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
491
+ }
486
492
  }
487
493
  }
488
494
  catch (error) {
@@ -492,6 +498,127 @@ ${toolsList}
492
498
  }
493
499
  return contents;
494
500
  }
501
+ /**
502
+ * Reads the content of a notebook cell.
503
+ * @param attachment The notebook attachment to read
504
+ * @returns Cell content as string or null if unable to read
505
+ */
506
+ async _readNotebookCells(attachment) {
507
+ if (attachment.type !== 'notebook' || !attachment.cells) {
508
+ return null;
509
+ }
510
+ try {
511
+ const model = await this.input.documentManager?.services.contents.get(attachment.value);
512
+ if (!model || model.type !== 'notebook') {
513
+ return null;
514
+ }
515
+ const kernelLang = model.content?.metadata?.language_info?.name ||
516
+ model.content?.metadata?.kernelspec?.language ||
517
+ 'text';
518
+ const selectedCells = attachment.cells
519
+ .map(cellInfo => {
520
+ const cell = model.content.cells.find((c) => c.id === cellInfo.id);
521
+ if (!cell) {
522
+ return null;
523
+ }
524
+ const code = cell.source || '';
525
+ const cellType = cell.cell_type;
526
+ const lang = cellType === 'code' ? kernelLang : cellType;
527
+ const DISPLAY_PRIORITY = [
528
+ 'application/vnd.jupyter.widget-view+json',
529
+ 'application/javascript',
530
+ 'text/html',
531
+ 'image/svg+xml',
532
+ 'image/png',
533
+ 'image/jpeg',
534
+ 'text/markdown',
535
+ 'text/latex',
536
+ 'text/plain'
537
+ ];
538
+ function extractDisplay(data) {
539
+ for (const mime of DISPLAY_PRIORITY) {
540
+ if (!(mime in data)) {
541
+ continue;
542
+ }
543
+ const value = data[mime];
544
+ if (!value) {
545
+ continue;
546
+ }
547
+ switch (mime) {
548
+ case 'application/vnd.jupyter.widget-view+json':
549
+ return `Widget: ${value.model_id ?? 'unknown model'}`;
550
+ case 'image/png':
551
+ return `![image](data:image/png;base64,${value.slice(0, 100)}...)`;
552
+ case 'image/jpeg':
553
+ return `![image](data:image/jpeg;base64,${value.slice(0, 100)}...)`;
554
+ case 'image/svg+xml':
555
+ return String(value).slice(0, 500) + '...\n[svg truncated]';
556
+ case 'text/html':
557
+ return (String(value).slice(0, 1000) +
558
+ (String(value).length > 1000 ? '\n...[truncated]' : ''));
559
+ case 'text/markdown':
560
+ case 'text/latex':
561
+ case 'text/plain': {
562
+ let text = Array.isArray(value)
563
+ ? value.join('')
564
+ : String(value);
565
+ if (text.length > 2000) {
566
+ text = text.slice(0, 2000) + '\n...[truncated]';
567
+ }
568
+ return text;
569
+ }
570
+ default:
571
+ return JSON.stringify(value).slice(0, 2000);
572
+ }
573
+ }
574
+ return JSON.stringify(data).slice(0, 2000);
575
+ }
576
+ let outputs = '';
577
+ if (cellType === 'code' && Array.isArray(cell.outputs)) {
578
+ outputs = cell.outputs
579
+ .map((output) => {
580
+ if (output.output_type === 'stream') {
581
+ return output.text;
582
+ }
583
+ else if (output.output_type === 'error') {
584
+ const err = output;
585
+ return `${err.ename}: ${err.evalue}\n${(err.traceback || []).join('\n')}`;
586
+ }
587
+ else if (output.output_type === 'execute_result' ||
588
+ output.output_type === 'display_data') {
589
+ const data = output.data;
590
+ if (!data) {
591
+ return '';
592
+ }
593
+ try {
594
+ return extractDisplay(data);
595
+ }
596
+ catch (e) {
597
+ console.error('Cannot extract cell output', e);
598
+ return '';
599
+ }
600
+ }
601
+ return '';
602
+ })
603
+ .filter(Boolean)
604
+ .join('\n---\n');
605
+ if (outputs.length > 2000) {
606
+ outputs = outputs.slice(0, 2000) + '\n...[truncated]';
607
+ }
608
+ }
609
+ return (`**Cell [${cellInfo.id}] (${cellType}):**\n` +
610
+ `\`\`\`${lang}\n${code}\n\`\`\`` +
611
+ (outputs ? `\n**Outputs:**\n\`\`\`text\n${outputs}\n\`\`\`` : ''));
612
+ })
613
+ .filter(Boolean)
614
+ .join('\n\n');
615
+ return `**Notebook: ${attachment.value}**\n${selectedCells}`;
616
+ }
617
+ catch (error) {
618
+ console.warn(`Failed to read notebook cells from ${attachment.value}:`, error);
619
+ return null;
620
+ }
621
+ }
495
622
  /**
496
623
  * Reads the content of a file attachment.
497
624
  * @param attachment The file attachment to read
@@ -2,18 +2,6 @@ import { NotebookPanel } from '@jupyterlab/notebook';
2
2
  import { generateText } from 'ai';
3
3
  import { createCompletionModel } from '../providers/models';
4
4
  import { SECRETS_NAMESPACE } from '../tokens';
5
- /**
6
- * Default system prompt for code completion
7
- */
8
- const DEFAULT_COMPLETION_SYSTEM_PROMPT = `You are an AI code completion assistant. Complete the given code fragment with appropriate code.
9
- Rules:
10
- - Return only the completion text, no explanations or comments
11
- - Do not include code block markers (\`\`\` or similar)
12
- - Make completions contextually relevant to the surrounding code and notebook context
13
- - Follow the language-specific conventions and style guidelines for the detected programming language
14
- - Keep completions concise but functional
15
- - Do not repeat the existing code that comes before the cursor
16
- - Use variables, imports, functions, and other definitions from previous notebook cells when relevant`;
17
5
  /**
18
6
  * Default temperature for code completion (lower than chat for more deterministic results)
19
7
  */
@@ -50,7 +38,7 @@ export class AICompletionProvider {
50
38
  * Get the system prompt for the completion.
51
39
  */
52
40
  get systemPrompt() {
53
- return DEFAULT_COMPLETION_SYSTEM_PROMPT;
41
+ return this._settingsModel.config.completionSystemPrompt;
54
42
  }
55
43
  /**
56
44
  * Fetch completion items based on the request and context.
@@ -0,0 +1,20 @@
1
+ import { AISettingsModel } from '../models/settings-model';
2
+ import { ReactWidget } from '@jupyterlab/ui-components';
3
+ /**
4
+ * The completion status props.
5
+ */
6
+ interface ICompletionStatusProps {
7
+ /**
8
+ * The settings model.
9
+ */
10
+ settingsModel: AISettingsModel;
11
+ }
12
+ /**
13
+ * The completion status widget that will be added to the status bar.
14
+ */
15
+ export declare class CompletionStatusWidget extends ReactWidget {
16
+ constructor(options: ICompletionStatusProps);
17
+ render(): JSX.Element;
18
+ private _props;
19
+ }
20
+ export {};
@@ -0,0 +1,51 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ReactWidget } from '@jupyterlab/ui-components';
3
+ import { jupyternautIcon } from '../icons';
4
+ const COMPLETION_STATUS_CLASS = 'jp-ai-completion-status';
5
+ const COMPLETION_DISABLED_CLASS = 'jp-ai-completion-disabled';
6
+ /**
7
+ * The completion status component.
8
+ */
9
+ function CompletionStatus(props) {
10
+ const [disabled, setDisabled] = useState(true);
11
+ const [title, setTitle] = useState('');
12
+ /**
13
+ * Handle changes in the settings.
14
+ */
15
+ useEffect(() => {
16
+ const stateChanged = (model) => {
17
+ if (model.config.useSameProviderForChatAndCompleter) {
18
+ setDisabled(false);
19
+ setTitle(`Completion using ${model.getDefaultProvider()?.model}`);
20
+ }
21
+ else if (model.config.activeCompleterProvider) {
22
+ setDisabled(false);
23
+ setTitle(`Completion using ${model.getProvider(model.config.activeCompleterProvider)?.model}`);
24
+ }
25
+ else {
26
+ setDisabled(true);
27
+ setTitle('No completion');
28
+ }
29
+ };
30
+ props.settingsModel.stateChanged.connect(stateChanged);
31
+ stateChanged(props.settingsModel);
32
+ return () => {
33
+ props.settingsModel.stateChanged.disconnect(stateChanged);
34
+ };
35
+ }, [props.settingsModel]);
36
+ return (React.createElement(jupyternautIcon.react, { className: disabled ? COMPLETION_DISABLED_CLASS : '', top: '2px', width: '16px', stylesheet: 'statusBar', title: title }));
37
+ }
38
+ /**
39
+ * The completion status widget that will be added to the status bar.
40
+ */
41
+ export class CompletionStatusWidget extends ReactWidget {
42
+ constructor(options) {
43
+ super();
44
+ this.addClass(COMPLETION_STATUS_CLASS);
45
+ this._props = options;
46
+ }
47
+ render() {
48
+ return React.createElement(CompletionStatus, { ...this._props });
49
+ }
50
+ _props;
51
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './clear-button';
2
+ export * from './completion-status';
2
3
  export * from './model-select';
3
4
  export * from './stop-button';
4
5
  export * from './token-usage-display';
@@ -1,4 +1,5 @@
1
1
  export * from './clear-button';
2
+ export * from './completion-status';
2
3
  export * from './model-select';
3
4
  export * from './stop-button';
4
5
  export * from './token-usage-display';