@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
@@ -1,4 +1,5 @@
1
1
  import { ReactWidget } from '@jupyterlab/ui-components';
2
+ import { Debouncer } from '@lumino/polling';
2
3
  import Add from '@mui/icons-material/Add';
3
4
  import Cable from '@mui/icons-material/Cable';
4
5
  import CheckCircle from '@mui/icons-material/CheckCircle';
@@ -10,7 +11,7 @@ import ErrorOutline from '@mui/icons-material/ErrorOutline';
10
11
  import MoreVert from '@mui/icons-material/MoreVert';
11
12
  import Settings from '@mui/icons-material/Settings';
12
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';
13
- import React, { useEffect, useState } from 'react';
14
+ import React, { useEffect, useMemo, useState } from 'react';
14
15
  import { SECRETS_NAMESPACE, SECRETS_REPLACEMENT } from '../tokens';
15
16
  import { ProviderConfigDialog } from './provider-config-dialog';
16
17
  /**
@@ -83,6 +84,10 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
83
84
  const [editingMCPServer, setEditingMCPServer] = useState();
84
85
  const [mcpMenuAnchor, setMcpMenuAnchor] = useState(null);
85
86
  const [mcpMenuServerId, setMcpMenuServerId] = useState('');
87
+ const [systemPromptValue, setSystemPromptValue] = useState(config.systemPrompt);
88
+ const systemPromptValueRef = React.useRef(config.systemPrompt);
89
+ const [completionPromptValue, setCompletionPromptValue] = useState(config.completionSystemPrompt);
90
+ const completionPromptValueRef = React.useRef(config.completionSystemPrompt);
86
91
  /**
87
92
  * Effect to listen for model state changes and update config
88
93
  */
@@ -129,10 +134,48 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
129
134
  agentManagerFactory.mcpConnectionChanged.disconnect(onMCPConnectionChanged);
130
135
  };
131
136
  }, [agentManagerFactory]);
137
+ // Sync local state when config changes externally
138
+ useEffect(() => {
139
+ setSystemPromptValue(config.systemPrompt);
140
+ systemPromptValueRef.current = config.systemPrompt;
141
+ }, [config.systemPrompt]);
142
+ useEffect(() => {
143
+ setCompletionPromptValue(config.completionSystemPrompt);
144
+ completionPromptValueRef.current = config.completionSystemPrompt;
145
+ }, [config.completionSystemPrompt]);
146
+ const promptDebouncer = useMemo(() => new Debouncer(async () => {
147
+ await handleConfigUpdate({
148
+ systemPrompt: systemPromptValueRef.current,
149
+ completionSystemPrompt: completionPromptValueRef.current
150
+ });
151
+ }, 1000), []);
152
+ // Cleanup debouncer on unmount
153
+ useEffect(() => {
154
+ return () => {
155
+ promptDebouncer.dispose();
156
+ };
157
+ }, [promptDebouncer]);
158
+ const handleSystemPromptChange = (value) => {
159
+ setSystemPromptValue(value);
160
+ systemPromptValueRef.current = value;
161
+ void promptDebouncer.invoke();
162
+ };
163
+ const handleCompletionPromptChange = (value) => {
164
+ setCompletionPromptValue(value);
165
+ completionPromptValueRef.current = value;
166
+ void promptDebouncer.invoke();
167
+ };
132
168
  const getSecretFromManager = async (provider, fieldName) => {
133
169
  const secret = await secretsManager?.get(Private.getToken(), SECRETS_NAMESPACE, `${provider}:${fieldName}`);
134
170
  return secret?.value;
135
171
  };
172
+ const setSecretToManager = async (provider, fieldName, value) => {
173
+ await secretsManager?.set(Private.getToken(), SECRETS_NAMESPACE, `${provider}:${fieldName}`, {
174
+ namespace: SECRETS_NAMESPACE,
175
+ id: `${provider}:${fieldName}`,
176
+ value
177
+ });
178
+ };
136
179
  /**
137
180
  * Attach a secrets field to the secrets manager.
138
181
  * @param input - the DOm element to attach.
@@ -188,9 +231,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
188
231
  // Retrieve the API key from the secrets manager if necessary.
189
232
  if (model.config.useSecretsManager && secretsManager) {
190
233
  provider.apiKey =
191
- (await getSecretFromManager(provider.provider, 'apiKey')) ??
192
- provider.apiKey ??
193
- '';
234
+ (await getSecretFromManager(provider.provider, 'apiKey')) ?? '';
194
235
  }
195
236
  setEditingProvider(provider);
196
237
  setDialogOpen(true);
@@ -227,6 +268,11 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
227
268
  if (updates.useSecretsManager !== undefined) {
228
269
  if (updates.useSecretsManager) {
229
270
  for (const provider of model.config.providers) {
271
+ // if the secrets manager doesn't have the current API key, copy the current
272
+ // one from settings.
273
+ if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
274
+ setSecretToManager(provider.provider, 'apiKey', provider.apiKey ?? '');
275
+ }
230
276
  provider.apiKey = SECRETS_REPLACEMENT;
231
277
  await model.updateProvider(provider.id, provider);
232
278
  }
@@ -307,6 +353,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
307
353
  overflow: 'auto',
308
354
  p: 2,
309
355
  pb: 4,
356
+ boxSizing: 'border-box',
310
357
  fontSize: '0.9rem'
311
358
  } },
312
359
  React.createElement(Box, { sx: { mb: 2, display: 'flex', alignItems: 'center', gap: 2 } },
@@ -318,11 +365,6 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
318
365
  React.createElement(Tab, { label: "Behavior" }),
319
366
  React.createElement(Tab, { label: "MCP Servers" }))),
320
367
  activeTab === 0 && (React.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 } },
321
- secretsManager !== undefined && (React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.useSecretsManager, onChange: e => handleConfigUpdate({
322
- useSecretsManager: e.target.checked
323
- }), color: "primary", sx: { alignSelf: 'flex-start' } }), label: React.createElement("div", null,
324
- React.createElement("span", null, "Use the secrets manager to manage API keys"),
325
- !config.useSecretsManager && (React.createElement(Alert, { severity: "warning", icon: React.createElement(Error, null), sx: { mb: 2 } }, "The secrets will be stored in plain text in settings"))) })),
326
368
  config.providers.length > 0 && (React.createElement(Card, { elevation: 2 },
327
369
  React.createElement(CardContent, null,
328
370
  React.createElement(Typography, { variant: "h6", component: "h2", gutterBottom: true }, "Default Providers"),
@@ -335,9 +377,9 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
335
377
  }), color: "primary" }), label: "Use same provider for chat and completions" }),
336
378
  !config.useSameProviderForChatAndCompleter && (React.createElement(FormControl, { fullWidth: true },
337
379
  React.createElement(InputLabel, null, "Completion Provider"),
338
- React.createElement(Select, { value: config.activeCompleterProvider || '', label: "Completion Provider", onChange: e => model.setActiveCompleterProvider(e.target.value || undefined) },
380
+ React.createElement(Select, { value: config.activeCompleterProvider || '', label: "Completion Provider", className: "jp-ai-completion-provider-select", onChange: e => model.setActiveCompleterProvider(e.target.value || undefined) },
339
381
  React.createElement(MenuItem, { value: "" },
340
- React.createElement("em", null, "Use chat provider")),
382
+ React.createElement("em", null, "No completion")),
341
383
  config.providers.map(provider => (React.createElement(MenuItem, { key: provider.id, value: provider.id }, provider.name)))))))))),
342
384
  React.createElement(Card, { elevation: 2 },
343
385
  React.createElement(CardContent, null,
@@ -398,7 +440,12 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
398
440
  params.maxTurns !== undefined && (React.createElement(Chip, { label: `Turns: ${params.maxTurns}`, size: "small", variant: "outlined" }))))),
399
441
  React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
400
442
  React.createElement(MoreVert, null)))));
401
- }))))))),
443
+ }))))),
444
+ secretsManager !== undefined && (React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.useSecretsManager, onChange: e => handleConfigUpdate({
445
+ useSecretsManager: e.target.checked
446
+ }), color: "primary", sx: { alignSelf: 'flex-start' } }), label: React.createElement("div", null,
447
+ React.createElement("span", null, "Use the secrets manager to manage API keys"),
448
+ !config.useSecretsManager && (React.createElement(Alert, { severity: "warning", icon: React.createElement(Error, null), sx: { mb: 2 } }, "The secrets are stored in plain text in settings"))) })))),
402
449
  activeTab === 1 && (React.createElement(Card, { elevation: 2 },
403
450
  React.createElement(CardContent, null,
404
451
  React.createElement(Typography, { variant: "h6", component: "h2", gutterBottom: true }, "Behavior Settings"),
@@ -418,8 +465,26 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
418
465
  }), color: "primary" }), label: React.createElement(Box, null,
419
466
  React.createElement(Typography, { variant: "body1" }, "Show Token Usage"),
420
467
  React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Display token usage information in the chat toolbar")) }),
468
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showCellDiff, onChange: e => handleConfigUpdate({
469
+ showCellDiff: e.target.checked
470
+ }), color: "primary" }), label: React.createElement(Box, null,
471
+ React.createElement(Typography, { variant: "body1" }, "Show Cell Diff"),
472
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Show diff view when AI modifies cell content")) }),
473
+ config.showCellDiff && (React.createElement(FormControl, { sx: { ml: 4 } },
474
+ React.createElement(InputLabel, null, "Diff Display Mode"),
475
+ React.createElement(Select, { value: config.diffDisplayMode, label: "Diff Display Mode", onChange: e => handleConfigUpdate({
476
+ diffDisplayMode: e.target.value
477
+ }) },
478
+ React.createElement(MenuItem, { value: "split" }, "Split View"),
479
+ React.createElement(MenuItem, { value: "unified" }, "Unified View")))),
480
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showFileDiff, onChange: e => handleConfigUpdate({
481
+ showFileDiff: e.target.checked
482
+ }), color: "primary" }), label: React.createElement(Box, null,
483
+ React.createElement(Typography, { variant: "body1" }, "Show File Diff"),
484
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Show diff view when AI modifies file content")) }),
421
485
  React.createElement(Divider, { sx: { my: 1 } }),
422
- React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: "System Prompt", value: config.systemPrompt, onChange: e => handleConfigUpdate({ systemPrompt: e.target.value }), placeholder: "Define the AI's behavior and personality...", helperText: "Instructions that define how the AI should behave and respond" }),
486
+ React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: "System Prompt", value: systemPromptValue, onChange: e => handleSystemPromptChange(e.target.value), placeholder: "Define the AI's behavior and personality...", helperText: "Instructions that define how the AI should behave and respond" }),
487
+ React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: "Completion System Prompt", value: completionPromptValue, onChange: e => handleCompletionPromptChange(e.target.value), placeholder: "Define how the AI should generate code completions...", helperText: "Instructions that define how the AI should generate code completions" }),
423
488
  React.createElement(Divider, { sx: { my: 2 } }),
424
489
  React.createElement(Box, null,
425
490
  React.createElement(Typography, { variant: "body1", gutterBottom: true }, "Commands Requiring Approval"),
@@ -1,7 +1,7 @@
1
1
  import ExpandMore from '@mui/icons-material/ExpandMore';
2
2
  import Visibility from '@mui/icons-material/Visibility';
3
3
  import VisibilityOff from '@mui/icons-material/VisibilityOff';
4
- import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
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
5
  import React from 'react';
6
6
  /**
7
7
  * Default parameter values for provider configuration
@@ -28,9 +28,10 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
28
28
  label: info.name,
29
29
  models: info.defaultModels,
30
30
  apiKeyRequirement: info.apiKeyRequirement,
31
- allowCustomModel: id === 'ollama' || id === 'generic', // Ollama and Generic allow custom models
31
+ allowCustomModel: id === 'generic', // Generic allows custom models
32
32
  supportsBaseURL: info.supportsBaseURL,
33
- description: info.description
33
+ description: info.description,
34
+ baseUrls: info.baseUrls
34
35
  };
35
36
  });
36
37
  }, [providerRegistry]);
@@ -124,11 +125,17 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
124
125
  endAdornment: (React.createElement(InputAdornment, { position: "end" },
125
126
  React.createElement(IconButton, { onClick: () => setShowApiKey(!showApiKey), edge: "end" }, showApiKey ? React.createElement(VisibilityOff, null) : React.createElement(Visibility, null))))
126
127
  } })),
127
- selectedProvider?.supportsBaseURL && (React.createElement(TextField, { fullWidth: true, label: "Base URL (Optional)", value: baseURL, onChange: e => setBaseURL(e.target.value), placeholder: provider === 'ollama'
128
- ? 'http://localhost:11434/api'
129
- : 'Custom API endpoint', helperText: provider === 'ollama'
130
- ? 'Ollama server endpoint'
131
- : 'Custom API base URL (e.g., for LiteLLM proxy). Leave empty to use default provider endpoint.' })),
128
+ selectedProvider?.supportsBaseURL && (React.createElement(Autocomplete, { freeSolo: true, fullWidth: true, options: (selectedProvider.baseUrls ?? []).map(option => option.url), value: baseURL || '', onChange: (_, value) => {
129
+ if (value && typeof value === 'string') {
130
+ setBaseURL(value);
131
+ }
132
+ }, inputValue: baseURL || '', renderOption: (props, option) => {
133
+ const urlOption = (selectedProvider.baseUrls ?? []).find(u => u.url === option);
134
+ return (React.createElement(Box, { component: "li", ...props, key: option },
135
+ React.createElement(Box, null,
136
+ React.createElement(Typography, { variant: "body2" }, option),
137
+ urlOption?.description && (React.createElement(Typography, { variant: "caption", color: "text.secondary" }, urlOption.description)))));
138
+ }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, label: "Base URL", placeholder: "https://api.example.com/v1", onChange: e => setBaseURL(e.target.value) })), clearOnBlur: false })),
132
139
  React.createElement(Accordion, { expanded: expandedAdvanced, onChange: (_, isExpanded) => setExpandedAdvanced(isExpanded), sx: {
133
140
  mt: 2,
134
141
  bgcolor: 'transparent',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/ai",
3
- "version": "0.9.0-a3",
3
+ "version": "0.9.0",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -54,10 +54,11 @@
54
54
  "watch:labextension": "jupyter labextension watch ."
55
55
  },
56
56
  "dependencies": {
57
- "@ai-sdk/anthropic": "^2.0.23",
57
+ "@ai-sdk/anthropic": "^2.0.30",
58
58
  "@ai-sdk/google": "^2.0.19",
59
59
  "@ai-sdk/mistral": "^2.0.17",
60
60
  "@ai-sdk/openai": "^2.0.44",
61
+ "@ai-sdk/openai-compatible": "^1.0.26",
61
62
  "@jupyter/chat": "^0.18.2",
62
63
  "@jupyterlab/application": "^4.0.0",
63
64
  "@jupyterlab/apputils": "^4.5.6",
@@ -71,11 +72,13 @@
71
72
  "@jupyterlab/rendermime": "^4.4.6",
72
73
  "@jupyterlab/services": "^7.4.6",
73
74
  "@jupyterlab/settingregistry": "^4.0.0",
75
+ "@jupyterlab/statusbar": "^4.4.6",
74
76
  "@jupyterlab/ui-components": "^4.4.6",
75
77
  "@lumino/commands": "^2.3.2",
76
78
  "@lumino/coreutils": "^2.2.1",
77
79
  "@lumino/disposable": "^2.1.4",
78
80
  "@lumino/messaging": "^2.0.3",
81
+ "@lumino/polling": "^2.1.4",
79
82
  "@lumino/signaling": "^2.1.4",
80
83
  "@lumino/widgets": "^2.7.1",
81
84
  "@modelcontextprotocol/sdk": "^1.19.1",
@@ -85,7 +88,6 @@
85
88
  "@openai/agents-extensions": "^0.1.5",
86
89
  "ai": "^5.0.60",
87
90
  "jupyter-secrets-manager": "^0.4.0",
88
- "ollama-ai-provider-v2": "^1.4.1",
89
91
  "zod": "^3.25.76"
90
92
  },
91
93
  "devDependencies": {
@@ -163,6 +163,31 @@
163
163
  "description": "Instructions that define how the AI should behave and respond",
164
164
  "type": "string",
165
165
  "default": "You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.\n\n## Your Core Mission\nYou'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.\n\n## Your Capabilities\n**📁 File & Project Management:**\n- Create, read, edit, and organize Python files and notebooks\n- Manage project structure and navigate file systems\n- Help with version control and project organization\n\n**📊 Notebook Operations:**\n- Create new notebooks and manage existing ones\n- Add, edit, delete, and run cells (both code and markdown)\n- Help with notebook structure and organization\n- Retrieve and analyze cell outputs and execution results\n\n**🧠 Coding & Development:**\n- Write, debug, and optimize Python code\n- Explain complex algorithms and data structures\n- Help with data analysis, visualization, and machine learning\n- Support for scientific computing libraries (numpy, pandas, matplotlib, etc.)\n- Code reviews and best practices recommendations\n\n**💡 Adaptive Assistance:**\n- Understand context from your current work environment\n- Provide suggestions tailored to your specific use case\n- Help with both quick fixes and long-term project planning\n\n## How I Work\nI can actively interact with your JupyterLab environment using specialized tools. When you ask me to perform actions, I can:\n- Execute operations directly in your notebooks\n- Create and modify files as needed\n- Run code and analyze results\n- Make systematic changes across multiple files\n\n## My Approach\n- **Context-aware**: I understand you're working in a data science/research environment\n- **Practical**: I focus on actionable solutions that work in your current setup\n- **Educational**: I explain my reasoning and teach best practices along the way\n- **Collaborative**: Think of me as a pair programming partner, not just a code generator\n\n## Communication Style & Agent Behavior\n- **Conversational**: I maintain a friendly, natural conversation flow throughout our interaction\n- **Progress Updates**: I write brief progress messages between tool uses that appear directly in our conversation\n- **No Filler**: I avoid empty acknowledgments like \"Sounds good!\" or \"Okay, I will...\" - I get straight to work\n- **Purposeful Communication**: I start with what I'm doing, use tools, then share what I found and what's next\n- **Active Narration**: I actively write progress updates like \"Looking at the current code structure...\" or \"Found the issue in the notebook...\" between tool calls\n- **Checkpoint Updates**: After several operations, I summarize what I've accomplished and what remains\n- **Natural Flow**: My explanations and progress reports appear as normal conversation text, not just in tool blocks\n\n## 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.\n\n## Technical Communication\n- Code is formatted in proper markdown blocks with syntax highlighting\n- Mathematical notation uses LaTeX formatting: \\\\(equations\\\\) and \\\\[display math\\\\]\n- I provide context for my actions and explain my reasoning as I work\n- When creating or modifying multiple files, I give brief summaries of changes\n- I keep users informed of progress while staying focused on the task\n\n## Multi-Step Task Handling\nWhen 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:\n- First use create_notebook to create the notebook\n- Then use add_code_cell or add_markdown_cell to add cells\n- Use set_cell_content to add content to cells as needed\n- Use run_cell to execute code when appropriate\n\nAlways think through multi-step tasks and use tools to fully complete the user's request rather than stopping after just one action.\n\nReady to help you build something great! What are you working on?"
166
+ },
167
+ "completionSystemPrompt": {
168
+ "title": "Completion System Prompt",
169
+ "description": "Instructions that define how the AI should generate code completions",
170
+ "type": "string",
171
+ "default": "You are an AI code completion assistant. Complete the given code fragment with appropriate code.\nRules:\n- Return only the completion text, no explanations or comments\n- Do not include code block markers (``` or similar)\n- Make completions contextually relevant to the surrounding code and notebook context\n- Follow the language-specific conventions and style guidelines for the detected programming language\n- Keep completions concise but functional\n- Do not repeat the existing code that comes before the cursor\n- Use variables, imports, functions, and other definitions from previous notebook cells when relevant"
172
+ },
173
+ "showCellDiff": {
174
+ "title": "Show Cell Diff",
175
+ "description": "Show diff view when AI modifies cell content",
176
+ "type": "boolean",
177
+ "default": true
178
+ },
179
+ "showFileDiff": {
180
+ "title": "Show File Diff",
181
+ "description": "Show diff view when AI modifies file content",
182
+ "type": "boolean",
183
+ "default": true
184
+ },
185
+ "diffDisplayMode": {
186
+ "title": "Diff Display Mode",
187
+ "description": "How to display cell diffs (split or unified view)",
188
+ "type": "string",
189
+ "enum": ["split", "unified"],
190
+ "default": "split"
166
191
  }
167
192
  },
168
193
  "additionalProperties": false
package/src/agent.ts CHANGED
@@ -184,18 +184,18 @@ export interface IAgentEventTypeMap {
184
184
  tool_call_start: {
185
185
  callId: string;
186
186
  toolName: string;
187
- input: any;
187
+ input: string;
188
188
  };
189
189
  tool_call_complete: {
190
190
  callId: string;
191
191
  toolName: string;
192
- output: any;
192
+ output: string;
193
193
  isError: boolean;
194
194
  };
195
195
  tool_approval_required: {
196
196
  interruptionId: string;
197
197
  toolName: string;
198
- toolInput: any;
198
+ toolInput: string;
199
199
  callId?: string;
200
200
  };
201
201
  grouped_approval_required: {
@@ -203,7 +203,7 @@ export interface IAgentEventTypeMap {
203
203
  approvals: Array<{
204
204
  interruptionId: string;
205
205
  toolName: string;
206
- toolInput: any;
206
+ toolInput: string;
207
207
  }>;
208
208
  };
209
209
  error: {
@@ -718,6 +718,22 @@ export class AgentManager {
718
718
  }
719
719
  }
720
720
 
721
+ /**
722
+ * Formats tool input for display by pretty-printing JSON strings.
723
+ * @param input The tool input string to format
724
+ * @returns Pretty-printed JSON string
725
+ */
726
+ private _formatToolInput(input: string): string {
727
+ try {
728
+ // Parse and re-stringify with formatting
729
+ const parsed = JSON.parse(input);
730
+ return JSON.stringify(parsed, null, 2);
731
+ } catch {
732
+ // If parsing fails, return the string as-is
733
+ return input;
734
+ }
735
+ }
736
+
721
737
  /**
722
738
  * Handles the start of a tool call from the model event.
723
739
  * @param modelEvent The model event containing tool call information
@@ -726,19 +742,13 @@ export class AgentManager {
726
742
  const toolCallId = modelEvent.toolCallId;
727
743
  const toolName = modelEvent.toolName;
728
744
  const toolInput = modelEvent.input;
729
- let parsedToolInput;
730
- try {
731
- parsedToolInput = JSON.parse(toolInput);
732
- } catch (error) {
733
- parsedToolInput = {};
734
- }
735
745
 
736
746
  this._agentEvent.emit({
737
747
  type: 'tool_call_start',
738
748
  data: {
739
749
  callId: toolCallId,
740
750
  toolName,
741
- input: parsedToolInput
751
+ input: this._formatToolInput(toolInput)
742
752
  }
743
753
  });
744
754
  }
@@ -758,7 +768,7 @@ export class AgentManager {
758
768
 
759
769
  const isError =
760
770
  toolCallOutput.rawItem.type === 'function_call_result' &&
761
- (toolCallOutput.rawItem as any).error;
771
+ toolCallOutput.rawItem.status === 'incomplete';
762
772
 
763
773
  const toolName =
764
774
  toolCallOutput.rawItem.type === 'function_call_result'
@@ -783,10 +793,13 @@ export class AgentManager {
783
793
  private async _handleSingleToolApproval(
784
794
  interruption: RunToolApprovalItem
785
795
  ): Promise<void> {
786
- const toolName = (interruption.rawItem as any)?.name || 'Unknown Tool';
787
- const toolInput = (interruption.rawItem as any)?.arguments || {};
796
+ const toolName = interruption.rawItem.name || 'Unknown Tool';
797
+ const toolInput = interruption.rawItem.arguments || '{}';
788
798
  const interruptionId = `int-${Date.now()}-${Math.random()}`;
789
- const callId = (interruption.rawItem as any)?.callId;
799
+ const callId =
800
+ interruption.rawItem.type === 'function_call'
801
+ ? interruption.rawItem.callId
802
+ : undefined;
790
803
 
791
804
  this._pendingApprovals.set(interruptionId, { interruption });
792
805
 
@@ -795,7 +808,7 @@ export class AgentManager {
795
808
  data: {
796
809
  interruptionId,
797
810
  toolName,
798
- toolInput,
811
+ toolInput: this._formatToolInput(toolInput),
799
812
  callId
800
813
  }
801
814
  });
@@ -810,8 +823,8 @@ export class AgentManager {
810
823
  ): Promise<void> {
811
824
  const groupId = `group-${Date.now()}-${Math.random()}`;
812
825
  const approvals = interruptions.map(interruption => {
813
- const toolName = (interruption.rawItem as any)?.name || 'Unknown Tool';
814
- const toolInput = (interruption.rawItem as any)?.arguments || {};
826
+ const toolName = interruption.rawItem.name || 'Unknown Tool';
827
+ const toolInput = interruption.rawItem.arguments || '{}';
815
828
  const interruptionId = `int-${Date.now()}-${Math.random()}`;
816
829
 
817
830
  this._pendingApprovals.set(interruptionId, { interruption, groupId });
@@ -819,7 +832,7 @@ export class AgentManager {
819
832
  return {
820
833
  interruptionId,
821
834
  toolName,
822
- toolInput
835
+ toolInput: this._formatToolInput(toolInput)
823
836
  };
824
837
  });
825
838
 
@@ -921,7 +934,9 @@ Guidelines:
921
934
  - Use natural, conversational tone throughout
922
935
 
923
936
  COMMAND DISCOVERY:
924
- - When you want to execute JupyterLab commands, ALWAYS use the 'discover_commands' tool first to find available commands and their metadata.
937
+ - 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.
938
+ - The query should typically be a single word, e.g., 'terminal', 'notebook', 'cell', 'file', 'edit', 'view', 'run', etc, to find relevant commands.
939
+ - 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.
925
940
  - 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.
926
941
 
927
942
  TOOL SELECTION GUIDELINES: