@jupyterlite/ai 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/lib/agent.d.ts +61 -7
  2. package/lib/agent.js +286 -103
  3. package/lib/chat-commands/clear.d.ts +8 -0
  4. package/lib/chat-commands/clear.js +30 -0
  5. package/lib/chat-commands/index.d.ts +2 -0
  6. package/lib/chat-commands/index.js +2 -0
  7. package/lib/chat-commands/skills.d.ts +19 -0
  8. package/lib/chat-commands/skills.js +57 -0
  9. package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
  10. package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
  11. package/lib/chat-model.d.ts +16 -0
  12. package/lib/chat-model.js +191 -11
  13. package/lib/completion/completion-provider.d.ts +1 -1
  14. package/lib/completion/completion-provider.js +14 -2
  15. package/lib/components/model-select.js +4 -4
  16. package/lib/components/tool-select.d.ts +11 -2
  17. package/lib/components/tool-select.js +77 -18
  18. package/lib/index.d.ts +3 -3
  19. package/lib/index.js +311 -72
  20. package/lib/models/settings-model.d.ts +3 -0
  21. package/lib/models/settings-model.js +63 -14
  22. package/lib/providers/built-in-providers.js +12 -7
  23. package/lib/providers/provider-tools.d.ts +36 -0
  24. package/lib/providers/provider-tools.js +93 -0
  25. package/lib/rendered-message-outputarea.d.ts +24 -0
  26. package/lib/rendered-message-outputarea.js +48 -0
  27. package/lib/skills/index.d.ts +4 -0
  28. package/lib/skills/index.js +7 -0
  29. package/lib/skills/parse-skill.d.ts +25 -0
  30. package/lib/skills/parse-skill.js +69 -0
  31. package/lib/skills/skill-loader.d.ts +25 -0
  32. package/lib/skills/skill-loader.js +133 -0
  33. package/lib/skills/skill-registry.d.ts +31 -0
  34. package/lib/skills/skill-registry.js +100 -0
  35. package/lib/skills/types.d.ts +29 -0
  36. package/lib/skills/types.js +5 -0
  37. package/lib/tokens.d.ts +77 -7
  38. package/lib/tokens.js +6 -1
  39. package/lib/tools/commands.js +4 -2
  40. package/lib/tools/skills.d.ts +9 -0
  41. package/lib/tools/skills.js +73 -0
  42. package/lib/tools/web.d.ts +8 -0
  43. package/lib/tools/web.js +196 -0
  44. package/lib/widgets/ai-settings.d.ts +1 -1
  45. package/lib/widgets/ai-settings.js +157 -38
  46. package/lib/widgets/main-area-chat.d.ts +6 -0
  47. package/lib/widgets/main-area-chat.js +28 -0
  48. package/lib/widgets/provider-config-dialog.js +207 -4
  49. package/package.json +18 -11
  50. package/schema/settings-model.json +97 -2
  51. package/src/agent.ts +397 -123
  52. package/src/chat-commands/clear.ts +46 -0
  53. package/src/chat-commands/index.ts +2 -0
  54. package/src/chat-commands/skills.ts +87 -0
  55. package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
  56. package/src/chat-model.ts +270 -23
  57. package/src/completion/completion-provider.ts +26 -12
  58. package/src/components/model-select.tsx +4 -5
  59. package/src/components/tool-select.tsx +110 -7
  60. package/src/index.ts +395 -87
  61. package/src/models/settings-model.ts +70 -15
  62. package/src/providers/built-in-providers.ts +12 -7
  63. package/src/providers/provider-tools.ts +179 -0
  64. package/src/rendered-message-outputarea.ts +62 -0
  65. package/src/skills/index.ts +14 -0
  66. package/src/skills/parse-skill.ts +91 -0
  67. package/src/skills/skill-loader.ts +175 -0
  68. package/src/skills/skill-registry.ts +137 -0
  69. package/src/skills/types.ts +37 -0
  70. package/src/tokens.ts +109 -9
  71. package/src/tools/commands.ts +4 -2
  72. package/src/tools/skills.ts +84 -0
  73. package/src/tools/web.ts +238 -0
  74. package/src/widgets/ai-settings.tsx +357 -77
  75. package/src/widgets/main-area-chat.ts +34 -1
  76. package/src/widgets/provider-config-dialog.tsx +496 -3
@@ -16,6 +16,7 @@ export class AISettingsModel extends VDomModel {
16
16
  showCellDiff: true,
17
17
  showFileDiff: true,
18
18
  diffDisplayMode: 'split',
19
+ skillsPaths: ['.agents/skills', '_agents/skills'],
19
20
  commandsRequiringApproval: [
20
21
  'notebook:restart-run-all',
21
22
  'notebook:run-cell',
@@ -32,6 +33,8 @@ export class AISettingsModel extends VDomModel {
32
33
  'runmenu:run-all',
33
34
  'jupyterlab-ai-commands:run-cell'
34
35
  ],
36
+ commandsAutoRenderMimeBundles: ['jupyterlab-ai-commands:execute-in-kernel'],
37
+ trustedMimeTypesForAutoRender: ['text/html'],
35
38
  systemPrompt: `You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.
36
39
 
37
40
  ## Your Core Mission
@@ -51,7 +54,7 @@ You're designed to be a capable partner for data science, research, and developm
51
54
 
52
55
  **⚡ Kernel Management:**
53
56
  - Start new kernels with specified language or kernel name
54
- - Execute code directly in running kernels without creating cells
57
+ - Execute code directly in a kernel using jupyterlab-ai-commands execution commands (not console), without creating cells
55
58
  - List running kernels and monitor their status
56
59
  - Manage kernel lifecycle (start, monitor, shutdown)
57
60
 
@@ -68,18 +71,32 @@ You're designed to be a capable partner for data science, research, and developm
68
71
  - Help with both quick fixes and long-term project planning
69
72
 
70
73
  ## How You Work
71
- You can actively interact with the user's JupyterLab environment using specialized tools. When asked to perform actions, you can:
72
- - Execute operations directly in notebooks
73
- - Create and modify files as needed
74
- - Run code and analyze results
75
- - Make systematic changes across multiple files
74
+ You interact with the user's JupyterLab environment primarily through the command system:
75
+ - Use 'discover_commands' to find available JupyterLab commands
76
+ - Use 'execute_command' to perform operations
77
+ - For file and notebook operations, use commands from the jupyterlab-ai-commands extension (prefixed with 'jupyterlab-ai-commands:')
78
+ - These commands provide comprehensive file and notebook manipulation: create, read, edit files/notebooks, manage cells, run code, etc.
79
+ - You can make systematic changes across multiple files and perform complex multi-step operations
80
+ - Skills are available via the skills tools: discover_skills (list) and load_skill (load instructions/resources)
81
+
82
+ ## Tool & Skill Use Policy
83
+ - When tools or skills are available and the task requires actions or environment-specific facts, use them instead of guessing
84
+ - Never guess command IDs. Always use discover_commands with a relevant query before execute_command, unless you already discovered the command earlier in this conversation
85
+ - If a preloaded skills snapshot is provided in the system prompt, use it instead of calling discover_skills to list skills
86
+ - Only call discover_skills if the user explicitly asks for the latest list or you need to verify a skill not in the snapshot
87
+ - When a skill is relevant, call load_skill with the skill name to load instructions; if it returns a non-empty resources array, load each listed resource with load_skill before proceeding
88
+ - If you're unsure how to perform a request, discover relevant commands (discover_commands with task keywords)
89
+ - Use a relevant skill even when the user doesn't explicitly mention it
90
+ - Prefer the single most relevant tool or skill; if multiple could apply, ask a brief clarifying question
91
+ - Ask for missing required inputs before calling a tool or skill
92
+ - Before calling a tool or skill, briefly state why you're calling it
76
93
 
77
94
  ## Code Execution Strategy
78
95
  When asked to run code or perform computations, choose the most appropriate approach:
79
- - **For quick computations or one-off code execution**: Use kernel commands to start a kernel and execute code directly, without creating notebook files. This is ideal for calculations, data lookups, or testing code snippets.
96
+ - **For quick computations or one-off code execution**: Use the kernel execution commands from jupyterlab-ai-commands to run code directly (no notebook/console). Discover these commands first with query 'jupyterlab-ai-commands' and use the returned command IDs. This is ideal for calculations, data lookups, or testing code snippets.
80
97
  - **For work that should be saved**: Create or use notebooks when the user needs a persistent record of their work, wants to iterate on code, or is building something they'll return to later.
81
98
 
82
- This means if the user asks you to "calculate the factorial of 100" or "check what library version is installed", run that directly in a kernel rather than creating a new notebook file.
99
+ This means if the user asks you to "calculate the factorial of 100" or "check what library version is installed", run that directly with the jupyterlab-ai-commands kernel execution command rather than creating a new notebook file.
83
100
 
84
101
  ## Your Approach
85
102
  - **Context-aware**: You understand the user is working in a data science/research environment
@@ -88,6 +105,23 @@ This means if the user asks you to "calculate the factorial of 100" or "check wh
88
105
  - **Collaborative**: You are a pair programming partner, not just a code generator
89
106
 
90
107
  ## Communication Style & Agent Behavior
108
+ IMPORTANT: Follow this message flow pattern for better user experience:
109
+
110
+ 1. FIRST: Explain what you're going to do and your approach
111
+ 2. THEN: Execute tools (these will show automatically with step numbers)
112
+ 3. FINALLY: Provide a concise summary of what was accomplished
113
+
114
+ Example flow:
115
+ - "I'll help you create a notebook with example cells. Let me first create the file structure, then add Python and Markdown cells."
116
+ - [Tool executions happen with automatic step display]
117
+ - "Successfully created your notebook with 3 cells: a title, code example, and visualization cell."
118
+
119
+ Guidelines:
120
+ - Start responses with your plan/approach before tool execution
121
+ - Let the system handle tool execution display (don't duplicate details)
122
+ - End with a brief summary of accomplishments
123
+ - Use natural, conversational tone throughout
124
+
91
125
  - **Conversational**: You maintain a friendly, natural conversation flow throughout the interaction
92
126
  - **Progress Updates**: You write brief progress messages between tool uses that appear directly in the conversation
93
127
  - **No Filler**: You avoid empty acknowledgments like "Sounds good!" or "Okay, I will..." - you get straight to work
@@ -106,13 +140,28 @@ This means if the user asks you to "calculate the factorial of 100" or "check wh
106
140
  - You keep users informed of progress while staying focused on the task
107
141
 
108
142
  ## Multi-Step Task Handling
109
- When users request complex tasks that require multiple steps (like "create a notebook with example cells"), you use tools in sequence to accomplish the complete task. For example:
110
- - First use create_notebook to create the notebook
111
- - Then use add_code_cell or add_markdown_cell to add cells
112
- - Use set_cell_content to add content to cells as needed
113
- - Use run_cell to execute code when appropriate
143
+ When users request complex tasks, you use the command system to accomplish them:
144
+ - For file and notebook operations, use discover_commands with query 'jupyterlab-ai-commands' to find the curated set of AI commands (~17 commands)
145
+ - For other JupyterLab operations (terminal, launcher, UI), use specific keywords like 'terminal', 'launcher', etc.
146
+ - IMPORTANT: Always use 'jupyterlab-ai-commands' as the query for file/notebook tasks - this returns a focused set instead of 100+ generic commands
147
+ - For example, to create a notebook with cells:
148
+ 1. discover_commands with query 'jupyterlab-ai-commands' to find available file/notebook commands
149
+ 2. execute_command with 'jupyterlab-ai-commands:create-notebook' and required arguments
150
+ 3. execute_command with 'jupyterlab-ai-commands:add-cell' multiple times to add cells
151
+ 4. execute_command with 'jupyterlab-ai-commands:set-cell-content' to add content to cells
152
+ 5. execute_command with 'jupyterlab-ai-commands:run-cell' when appropriate
153
+
154
+ ## Kernel Preference for Notebooks and Consoles
155
+ When creating notebooks or consoles for a specific programming language, use the 'kernelPreference' argument:
156
+ Only create consoles when the user explicitly asks for one; otherwise prefer the jupyterlab-ai-commands kernel execution commands for running code.
157
+ - To specify by language: { "kernelPreference": { "language": "python" } } or { "kernelPreference": { "language": "julia" } }
158
+ - To specify by kernel name: { "kernelPreference": { "name": "python3" } } or { "kernelPreference": { "name": "julia-1.10" } }
159
+ - Example: execute_command with commandId="notebook:create-new" and args={ "kernelPreference": { "language": "python" } }
160
+ - Example: execute_command with commandId="console:create" and args={ "kernelPreference": { "name": "python3" } }
161
+ - Common kernel names: "python3" (Python), "julia-1.10" (Julia), "ir" (R), "xpython" (xeus-python)
162
+ - If unsure of exact kernel name, prefer using "language" which will match any kernel supporting that language
114
163
 
115
- Always think through multi-step tasks and use tools to fully complete the user's request rather than stopping after just one action.
164
+ Always think through multi-step tasks and use commands to fully complete the user's request rather than stopping after just one action.
116
165
 
117
166
  You are ready to help users build something great!`,
118
167
  // Completion system prompt - also defined in schema/settings-model.json
@@ -11,6 +11,7 @@ export const anthropicProvider = {
11
11
  name: 'Anthropic Claude',
12
12
  apiKeyRequirement: 'required',
13
13
  defaultModels: [
14
+ 'claude-opus-4-6',
14
15
  'claude-opus-4-5',
15
16
  'claude-opus-4-5-20251101',
16
17
  'claude-sonnet-4-5',
@@ -31,6 +32,10 @@ export const anthropicProvider = {
31
32
  ],
32
33
  supportsBaseURL: true,
33
34
  supportsHeaders: true,
35
+ providerToolCapabilities: {
36
+ webSearch: { implementation: 'anthropic' },
37
+ webFetch: { implementation: 'anthropic' }
38
+ },
34
39
  factory: (options) => {
35
40
  if (!options.apiKey) {
36
41
  throw new Error('API key required for Anthropic');
@@ -60,7 +65,7 @@ export const googleProvider = {
60
65
  'gemini-3-flash-preview',
61
66
  'gemini-2.5-pro',
62
67
  'gemini-2.5-flash',
63
- 'gemini-2.5-flash-image-preview',
68
+ 'gemini-2.5-flash-image',
64
69
  'gemini-2.5-flash-lite',
65
70
  'gemini-2.5-flash-lite-preview-09-2025',
66
71
  'gemini-2.5-flash-preview-04-17',
@@ -154,21 +159,18 @@ export const openaiProvider = {
154
159
  'gpt-5.2',
155
160
  'gpt-5.2-chat-latest',
156
161
  'gpt-5.2-pro',
162
+ 'gpt-5.2-codex',
157
163
  'gpt-5.1',
158
164
  'gpt-5.1-chat-latest',
159
- 'gpt-5.1-codex',
160
- 'gpt-5.1-codex-mini',
161
- 'gpt-5.1-codex-max',
162
165
  'gpt-5',
163
166
  'gpt-5-2025-08-07',
164
167
  'gpt-5-chat-latest',
165
- 'gpt-5-codex',
166
- 'gpt-5-pro',
167
- 'gpt-5-pro-2025-10-06',
168
168
  'gpt-5-mini',
169
169
  'gpt-5-mini-2025-08-07',
170
170
  'gpt-5-nano',
171
171
  'gpt-5-nano-2025-08-07',
172
+ 'o4-mini',
173
+ 'o4-mini-2025-04-16',
172
174
  'o3',
173
175
  'o3-2025-04-16',
174
176
  'o3-mini',
@@ -200,6 +202,9 @@ export const openaiProvider = {
200
202
  ],
201
203
  supportsBaseURL: true,
202
204
  supportsHeaders: true,
205
+ providerToolCapabilities: {
206
+ webSearch: { implementation: 'openai' }
207
+ },
203
208
  factory: (options) => {
204
209
  if (!options.apiKey) {
205
210
  throw new Error('API key required for OpenAI');
@@ -0,0 +1,36 @@
1
+ import type { Tool } from 'ai';
2
+ import type { IProviderInfo } from '../tokens';
3
+ type ToolMap = Record<string, Tool>;
4
+ interface IWebSearchSettings {
5
+ enabled?: boolean;
6
+ externalWebAccess?: boolean;
7
+ searchContextSize?: 'low' | 'medium' | 'high';
8
+ allowedDomains?: string[];
9
+ blockedDomains?: string[];
10
+ maxUses?: number;
11
+ }
12
+ interface IWebFetchSettings {
13
+ enabled?: boolean;
14
+ maxUses?: number;
15
+ maxContentTokens?: number;
16
+ allowedDomains?: string[];
17
+ blockedDomains?: string[];
18
+ citationsEnabled?: boolean;
19
+ }
20
+ /**
21
+ * Provider-level custom settings that control built-in web tools.
22
+ */
23
+ export interface IProviderCustomSettings {
24
+ webSearch?: IWebSearchSettings;
25
+ webFetch?: IWebFetchSettings;
26
+ }
27
+ interface IProviderToolContext {
28
+ providerInfo?: IProviderInfo | null;
29
+ customSettings?: IProviderCustomSettings;
30
+ hasFunctionTools: boolean;
31
+ }
32
+ /**
33
+ * Create provider-defined tools from custom settings and provider capabilities.
34
+ */
35
+ export declare function createProviderTools(options: IProviderToolContext): ToolMap;
36
+ export {};
@@ -0,0 +1,93 @@
1
+ import { anthropic } from '@ai-sdk/anthropic';
2
+ import { openai } from '@ai-sdk/openai';
3
+ const DEFAULT_ANTHROPIC_WEB_FETCH_MAX_USES = 2;
4
+ const DEFAULT_ANTHROPIC_WEB_FETCH_MAX_CONTENT_TOKENS = 12000;
5
+ function normalizeDomain(value) {
6
+ const normalized = (value || '').trim().toLowerCase();
7
+ const withoutProtocol = normalized.replace(/^https?:\/\//, '');
8
+ const hostname = withoutProtocol.split('/')[0].trim();
9
+ // Treat "*.example.com" as "example.com" for provider domain filters.
10
+ return hostname.startsWith('*.') ? hostname.slice(2) : hostname;
11
+ }
12
+ function collectDomains(value) {
13
+ value = value || [];
14
+ const values = Array.from(new Set(value.map(normalizeDomain).filter(domain => domain.length > 0)));
15
+ return values;
16
+ }
17
+ function createOpenAIWebSearchTool(webSearchSettings) {
18
+ const allowedDomains = collectDomains(webSearchSettings.allowedDomains);
19
+ return openai.tools.webSearch({
20
+ externalWebAccess: webSearchSettings.externalWebAccess,
21
+ searchContextSize: webSearchSettings.searchContextSize,
22
+ filters: allowedDomains.length > 0 ? { allowedDomains } : undefined
23
+ });
24
+ }
25
+ function createAnthropicWebSearchTool(webSearchSettings) {
26
+ const allowedDomains = collectDomains(webSearchSettings.allowedDomains);
27
+ const blockedDomains = collectDomains(webSearchSettings.blockedDomains);
28
+ return anthropic.tools.webSearch_20250305({
29
+ maxUses: webSearchSettings.maxUses,
30
+ allowedDomains: allowedDomains.length > 0 ? allowedDomains : undefined,
31
+ blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
32
+ });
33
+ }
34
+ function createAnthropicWebFetchTool(webFetchSettings) {
35
+ const maxUses = webFetchSettings.maxUses ?? DEFAULT_ANTHROPIC_WEB_FETCH_MAX_USES;
36
+ const maxContentTokens = webFetchSettings.maxContentTokens ??
37
+ DEFAULT_ANTHROPIC_WEB_FETCH_MAX_CONTENT_TOKENS;
38
+ const allowedDomains = collectDomains(webFetchSettings.allowedDomains);
39
+ const blockedDomains = collectDomains(webFetchSettings.blockedDomains);
40
+ const citationsEnabled = webFetchSettings.citationsEnabled;
41
+ return anthropic.tools.webFetch_20250910({
42
+ maxUses,
43
+ maxContentTokens,
44
+ allowedDomains: allowedDomains.length > 0 ? allowedDomains : undefined,
45
+ blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined,
46
+ citations: citationsEnabled !== undefined ? { enabled: citationsEnabled } : undefined
47
+ });
48
+ }
49
+ function createWebSearchTool(implementation, webSearchSettings) {
50
+ switch (implementation) {
51
+ case 'openai':
52
+ return createOpenAIWebSearchTool(webSearchSettings);
53
+ case 'anthropic':
54
+ return createAnthropicWebSearchTool(webSearchSettings);
55
+ default:
56
+ throw new Error(`Unsupported web search implementation: ${implementation}`);
57
+ }
58
+ }
59
+ function createWebFetchTool(implementation, webFetchSettings) {
60
+ switch (implementation) {
61
+ case 'anthropic':
62
+ return createAnthropicWebFetchTool(webFetchSettings);
63
+ default:
64
+ throw new Error(`Unsupported web fetch implementation: ${implementation}`);
65
+ }
66
+ }
67
+ /**
68
+ * Create provider-defined tools from custom settings and provider capabilities.
69
+ */
70
+ export function createProviderTools(options) {
71
+ const tools = {};
72
+ if (!options.customSettings ||
73
+ !options.providerInfo?.providerToolCapabilities) {
74
+ return tools;
75
+ }
76
+ const capabilities = options.providerInfo.providerToolCapabilities;
77
+ const webSearchSettings = options.customSettings.webSearch;
78
+ const webFetchSettings = options.customSettings.webFetch;
79
+ const webSearchEnabled = webSearchSettings?.enabled === true;
80
+ const webFetchEnabled = webFetchSettings?.enabled === true;
81
+ const webSearchCapability = capabilities.webSearch;
82
+ if (webSearchEnabled && webSearchSettings && webSearchCapability) {
83
+ const requiresNoFunctionTools = webSearchCapability.requiresNoFunctionTools === true;
84
+ if (!requiresNoFunctionTools || !options.hasFunctionTools) {
85
+ tools.web_search = createWebSearchTool(webSearchCapability.implementation, webSearchSettings);
86
+ }
87
+ }
88
+ const webFetchCapability = capabilities.webFetch;
89
+ if (webFetchEnabled && webFetchSettings && webFetchCapability) {
90
+ tools.web_fetch = createWebFetchTool(webFetchCapability.implementation, webFetchSettings);
91
+ }
92
+ return tools;
93
+ }
@@ -0,0 +1,24 @@
1
+ import { ChatWidget } from '@jupyter/chat';
2
+ import { IDisposable } from '@lumino/disposable';
3
+ /**
4
+ * Ensures chat-rendered MIME outputs also expose the OutputArea class so
5
+ * renderer extensions can reuse their notebook/output-area CSS rules.
6
+ *
7
+ * TODO: Remove this compatibility layer once jupyter-chat applies
8
+ * `jp-OutputArea` (or equivalent output-area context) to rendered MIME
9
+ * messages by default.
10
+ */
11
+ export declare class RenderedMessageOutputAreaCompat implements IDisposable {
12
+ constructor(options: RenderedMessageOutputAreaCompat.IOptions);
13
+ get isDisposed(): boolean;
14
+ dispose(): void;
15
+ private _scheduleSync;
16
+ private readonly _chatPanel;
17
+ private _isDisposed;
18
+ private _raf;
19
+ }
20
+ export declare namespace RenderedMessageOutputAreaCompat {
21
+ interface IOptions {
22
+ chatPanel: ChatWidget;
23
+ }
24
+ }
@@ -0,0 +1,48 @@
1
+ const OUTPUT_AREA_CLASS = 'jp-OutputArea';
2
+ const CHAT_RENDERED_MESSAGE_SELECTOR = `.jp-chat-rendered-message:not(.${OUTPUT_AREA_CLASS})`;
3
+ /**
4
+ * Ensures chat-rendered MIME outputs also expose the OutputArea class so
5
+ * renderer extensions can reuse their notebook/output-area CSS rules.
6
+ *
7
+ * TODO: Remove this compatibility layer once jupyter-chat applies
8
+ * `jp-OutputArea` (or equivalent output-area context) to rendered MIME
9
+ * messages by default.
10
+ */
11
+ export class RenderedMessageOutputAreaCompat {
12
+ constructor(options) {
13
+ this._chatPanel = options.chatPanel;
14
+ this._chatPanel.model.messagesUpdated.connect(this._scheduleSync, this);
15
+ this._scheduleSync();
16
+ }
17
+ get isDisposed() {
18
+ return this._isDisposed;
19
+ }
20
+ dispose() {
21
+ if (this._isDisposed) {
22
+ return;
23
+ }
24
+ this._isDisposed = true;
25
+ this._chatPanel.model.messagesUpdated.disconnect(this._scheduleSync, this);
26
+ if (this._raf !== 0) {
27
+ cancelAnimationFrame(this._raf);
28
+ this._raf = 0;
29
+ }
30
+ }
31
+ _scheduleSync() {
32
+ if (this._isDisposed || this._raf !== 0) {
33
+ return;
34
+ }
35
+ this._raf = requestAnimationFrame(() => {
36
+ this._raf = 0;
37
+ if (this._isDisposed) {
38
+ return;
39
+ }
40
+ this._chatPanel.node
41
+ .querySelectorAll(CHAT_RENDERED_MESSAGE_SELECTOR)
42
+ .forEach(element => element.classList.add(OUTPUT_AREA_CLASS));
43
+ });
44
+ }
45
+ _chatPanel;
46
+ _isDisposed = false;
47
+ _raf = 0;
48
+ }
@@ -0,0 +1,4 @@
1
+ export { parseSkillMd, type IParsedSkill } from './parse-skill';
2
+ export { loadSkillsFromPaths, type ISkillFileDefinition } from './skill-loader';
3
+ export type { ISkillDefinition, ISkillRegistration, ISkillResourceResult, ISkillSummary } from './types';
4
+ export { SkillRegistry } from './skill-registry';
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export { parseSkillMd } from './parse-skill';
6
+ export { loadSkillsFromPaths } from './skill-loader';
7
+ export { SkillRegistry } from './skill-registry';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Parsed skill definition from a SKILL.md file.
3
+ */
4
+ export interface IParsedSkill {
5
+ name: string;
6
+ description: string;
7
+ instructions: string;
8
+ }
9
+ /**
10
+ * Parse a SKILL.md file content into a structured skill definition.
11
+ *
12
+ * Expected format:
13
+ * ```
14
+ * ---
15
+ * name: my-skill
16
+ * description: A brief description of the skill
17
+ * ---
18
+ * Full instructions body here...
19
+ * ```
20
+ *
21
+ * @param content - The raw content of a SKILL.md file
22
+ * @returns Parsed skill with name, description, and instructions
23
+ * @throws Error if the frontmatter is missing or invalid
24
+ */
25
+ export declare function parseSkillMd(content: string): IParsedSkill;
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { parse as parseYaml } from 'yaml';
6
+ /**
7
+ * Parse a SKILL.md file content into a structured skill definition.
8
+ *
9
+ * Expected format:
10
+ * ```
11
+ * ---
12
+ * name: my-skill
13
+ * description: A brief description of the skill
14
+ * ---
15
+ * Full instructions body here...
16
+ * ```
17
+ *
18
+ * @param content - The raw content of a SKILL.md file
19
+ * @returns Parsed skill with name, description, and instructions
20
+ * @throws Error if the frontmatter is missing or invalid
21
+ */
22
+ export function parseSkillMd(content) {
23
+ const normalizedContent = content
24
+ .replace(/^\uFEFF/, '')
25
+ .replace(/\r\n/g, '\n');
26
+ const lines = normalizedContent.split('\n');
27
+ if (lines[0]?.trim() !== '---') {
28
+ throw new Error('Invalid SKILL.md: missing frontmatter delimiters (---)');
29
+ }
30
+ const frontmatterLines = [];
31
+ let i = 1;
32
+ for (; i < lines.length; i++) {
33
+ const line = lines[i];
34
+ const trimmed = line.trim();
35
+ if (trimmed === '---') {
36
+ break;
37
+ }
38
+ frontmatterLines.push(line);
39
+ }
40
+ if (i >= lines.length) {
41
+ throw new Error('Invalid SKILL.md: missing frontmatter delimiters (---)');
42
+ }
43
+ const frontmatter = frontmatterLines.join('\n');
44
+ const instructions = lines
45
+ .slice(i + 1)
46
+ .join('\n')
47
+ .trim();
48
+ let metadata;
49
+ try {
50
+ metadata = parseYaml(frontmatter);
51
+ }
52
+ catch (error) {
53
+ throw new Error(`Invalid SKILL.md: YAML frontmatter parse failed: ${error}`);
54
+ }
55
+ const data = metadata;
56
+ const name = data?.name;
57
+ const description = data?.description;
58
+ if (typeof name !== 'string' || name.trim().length === 0) {
59
+ throw new Error('Invalid SKILL.md: missing "name" in frontmatter');
60
+ }
61
+ if (typeof description !== 'string' || description.trim().length === 0) {
62
+ throw new Error('Invalid SKILL.md: missing "description" in frontmatter');
63
+ }
64
+ return {
65
+ name: name.trim(),
66
+ description: description.trim(),
67
+ instructions
68
+ };
69
+ }
@@ -0,0 +1,25 @@
1
+ import { Contents } from '@jupyterlab/services';
2
+ import { IParsedSkill } from './parse-skill';
3
+ /**
4
+ * A skill definition loaded from the filesystem.
5
+ */
6
+ export interface ISkillFileDefinition extends IParsedSkill {
7
+ /**
8
+ * Path to the skill directory (e.g. ".agents/skills/my-skill").
9
+ */
10
+ path: string;
11
+ /**
12
+ * Paths to resource files relative to the skill directory.
13
+ */
14
+ resources: string[];
15
+ }
16
+ /**
17
+ * Load skills from multiple directories. Each path is scanned in order;
18
+ * when the same skill name appears in more than one path, the first
19
+ * occurrence wins.
20
+ *
21
+ * @param contentsManager - The Jupyter contents manager
22
+ * @param skillsPaths - Ordered list of directories to scan
23
+ * @returns Merged array of loaded skill definitions
24
+ */
25
+ export declare function loadSkillsFromPaths(contentsManager: Contents.IManager, skillsPaths: string[]): Promise<ISkillFileDefinition[]>;
@@ -0,0 +1,133 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { PathExt } from '@jupyterlab/coreutils';
6
+ import { parseSkillMd } from './parse-skill';
7
+ /**
8
+ * Load skills from multiple directories. Each path is scanned in order;
9
+ * when the same skill name appears in more than one path, the first
10
+ * occurrence wins.
11
+ *
12
+ * @param contentsManager - The Jupyter contents manager
13
+ * @param skillsPaths - Ordered list of directories to scan
14
+ * @returns Merged array of loaded skill definitions
15
+ */
16
+ export async function loadSkillsFromPaths(contentsManager, skillsPaths) {
17
+ const seen = new Set();
18
+ const merged = [];
19
+ for (const skillsPath of skillsPaths) {
20
+ const skills = await loadSkills(contentsManager, skillsPath);
21
+ for (const skill of skills) {
22
+ if (seen.has(skill.name)) {
23
+ console.debug(`Skipping duplicate skill "${skill.name}" from "${skillsPath}" (already loaded from an earlier path).`);
24
+ continue;
25
+ }
26
+ seen.add(skill.name);
27
+ merged.push(skill);
28
+ }
29
+ }
30
+ return merged;
31
+ }
32
+ /**
33
+ * Load skills from the filesystem by scanning a directory for subdirectories
34
+ * containing SKILL.md files.
35
+ *
36
+ * @param contentsManager - The Jupyter contents manager
37
+ * @param skillsPath - Path to the skills directory (e.g. ".agents/skills")
38
+ * @returns Array of loaded skill definitions
39
+ */
40
+ async function loadSkills(contentsManager, skillsPath) {
41
+ const skills = [];
42
+ // Walk each path segment from root to verify the directory exists before fetching it.
43
+ const segments = skillsPath.split('/').filter(s => s.length > 0);
44
+ let currentPath = '';
45
+ for (const segment of segments) {
46
+ let listing;
47
+ try {
48
+ listing = await contentsManager.get(currentPath, { content: true });
49
+ }
50
+ catch (error) {
51
+ console.debug(`Skills path segment not found at "${currentPath}":`, error);
52
+ return skills;
53
+ }
54
+ const children = (listing.content ?? []);
55
+ if (!children.some(c => c.type === 'directory' && c.name === segment)) {
56
+ return skills;
57
+ }
58
+ currentPath = PathExt.join(currentPath, segment);
59
+ }
60
+ const dirModel = await contentsManager.get(skillsPath, { content: true });
61
+ if (dirModel.type !== 'directory' || !dirModel.content) {
62
+ return skills;
63
+ }
64
+ for (const child of dirModel.content) {
65
+ if (child.type !== 'directory') {
66
+ continue;
67
+ }
68
+ // List the subdirectory to check if SKILL.md exists before requesting it
69
+ const subDir = await contentsManager.get(child.path, { content: true });
70
+ const subChildren = (subDir.content ?? []);
71
+ if (!subChildren.some(f => f.type === 'file' && f.name === 'SKILL.md')) {
72
+ continue;
73
+ }
74
+ const skillMdPath = `${child.path}/SKILL.md`;
75
+ const fileModel = await contentsManager.get(skillMdPath, {
76
+ content: true
77
+ });
78
+ if (typeof fileModel.content !== 'string') {
79
+ console.warn(`Skipping ${skillMdPath}: content is not a string`);
80
+ continue;
81
+ }
82
+ try {
83
+ const parsed = parseSkillMd(fileModel.content);
84
+ const resources = await collectResourcePaths(contentsManager, child.path);
85
+ skills.push({
86
+ ...parsed,
87
+ path: child.path,
88
+ resources
89
+ });
90
+ }
91
+ catch (error) {
92
+ console.warn(`Skipping skill at ${child.path}:`, error);
93
+ }
94
+ }
95
+ return skills;
96
+ }
97
+ /**
98
+ * Recursively collect paths to all resource files in a skill directory,
99
+ * excluding `SKILL.md`. Content is loaded on-demand when the agent
100
+ * requests a specific resource.
101
+ */
102
+ async function collectResourcePaths(contentsManager, basePath) {
103
+ const resourcePaths = [];
104
+ async function walk(dirPath) {
105
+ let dirModel;
106
+ try {
107
+ dirModel = await contentsManager.get(dirPath, { content: true });
108
+ }
109
+ catch (error) {
110
+ console.warn(`Failed to list directory ${dirPath}:`, error);
111
+ return;
112
+ }
113
+ if (dirModel.type !== 'directory' || !dirModel.content) {
114
+ return;
115
+ }
116
+ for (const item of dirModel.content) {
117
+ // Skip checkpoint directories
118
+ if (item.name === '.ipynb_checkpoints') {
119
+ continue;
120
+ }
121
+ if (item.type === 'directory') {
122
+ await walk(item.path);
123
+ }
124
+ else if (item.type === 'file' && item.name !== 'SKILL.md') {
125
+ // Store path relative to the skill directory
126
+ const relativePath = PathExt.relative(basePath, item.path);
127
+ resourcePaths.push(relativePath);
128
+ }
129
+ }
130
+ }
131
+ await walk(basePath);
132
+ return resourcePaths;
133
+ }