@theia/ai-core 1.73.0-next.7 → 1.73.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.
- package/README.md +2 -2
- package/lib/browser/{agent-completion-notification-service.d.ts → agent-notification-service.d.ts} +12 -10
- package/lib/browser/agent-notification-service.d.ts.map +1 -0
- package/lib/browser/{agent-completion-notification-service.js → agent-notification-service.js} +37 -27
- package/lib/browser/agent-notification-service.js.map +1 -0
- package/lib/browser/ai-core-frontend-module.js +2 -2
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.js +3 -3
- package/lib/browser/frontend-prompt-customization-service.d.ts +193 -16
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +686 -46
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.spec.js +259 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
- package/lib/browser/frontend-variable-service.d.ts +2 -1
- package/lib/browser/frontend-variable-service.d.ts.map +1 -1
- package/lib/browser/frontend-variable-service.js +6 -1
- package/lib/browser/frontend-variable-service.js.map +1 -1
- package/lib/browser/index.d.ts +1 -1
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/os-notification-service.d.ts +10 -6
- package/lib/browser/os-notification-service.d.ts.map +1 -1
- package/lib/browser/os-notification-service.js +34 -20
- package/lib/browser/os-notification-service.js.map +1 -1
- package/lib/browser/prompt-variable-contribution.spec.js +2 -1
- package/lib/browser/prompt-variable-contribution.spec.js.map +1 -1
- package/lib/browser/skill-service.d.ts +6 -1
- package/lib/browser/skill-service.d.ts.map +1 -1
- package/lib/browser/skill-service.js +47 -18
- package/lib/browser/skill-service.js.map +1 -1
- package/lib/browser/skill-service.spec.js +153 -31
- package/lib/browser/skill-service.spec.js.map +1 -1
- package/lib/browser/token-usage-frontend-service-impl.d.ts +2 -0
- package/lib/browser/token-usage-frontend-service-impl.d.ts.map +1 -1
- package/lib/browser/token-usage-frontend-service-impl.js +6 -1
- package/lib/browser/token-usage-frontend-service-impl.js.map +1 -1
- package/lib/browser/window-blink-service.d.ts +2 -0
- package/lib/browser/window-blink-service.d.ts.map +1 -1
- package/lib/browser/window-blink-service.js +8 -3
- package/lib/browser/window-blink-service.js.map +1 -1
- package/lib/common/agent-preferences.d.ts.map +1 -1
- package/lib/common/agent-preferences.js +12 -2
- package/lib/common/agent-preferences.js.map +1 -1
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +4 -2
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/capability-variable-contribution.spec.js +2 -1
- package/lib/common/capability-variable-contribution.spec.js.map +1 -1
- package/lib/common/frontmatter.d.ts +33 -0
- package/lib/common/frontmatter.d.ts.map +1 -0
- package/lib/common/frontmatter.js +80 -0
- package/lib/common/frontmatter.js.map +1 -0
- package/lib/common/frontmatter.spec.d.ts +2 -0
- package/lib/common/frontmatter.spec.d.ts.map +1 -0
- package/lib/common/frontmatter.spec.js +73 -0
- package/lib/common/frontmatter.spec.js.map +1 -0
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-service.js +1 -1
- package/lib/common/language-model-service.js.map +1 -1
- package/lib/common/language-model-service.spec.d.ts +2 -0
- package/lib/common/language-model-service.spec.d.ts.map +1 -0
- package/lib/common/language-model-service.spec.js +62 -0
- package/lib/common/language-model-service.spec.js.map +1 -0
- package/lib/common/language-model.d.ts +64 -2
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +8 -2
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/notification-types.d.ts +7 -0
- package/lib/common/notification-types.d.ts.map +1 -1
- package/lib/common/notification-types.js +7 -1
- package/lib/common/notification-types.js.map +1 -1
- package/lib/common/prompt-service-util.d.ts +22 -1
- package/lib/common/prompt-service-util.d.ts.map +1 -1
- package/lib/common/prompt-service-util.js +24 -2
- package/lib/common/prompt-service-util.js.map +1 -1
- package/lib/common/prompt-service-util.spec.d.ts +2 -0
- package/lib/common/prompt-service-util.spec.d.ts.map +1 -0
- package/lib/common/prompt-service-util.spec.js +54 -0
- package/lib/common/prompt-service-util.spec.js.map +1 -0
- package/lib/common/prompt-service.d.ts +72 -8
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +36 -4
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +49 -2
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/settings-service.d.ts +5 -0
- package/lib/common/settings-service.d.ts.map +1 -1
- package/lib/common/skill.d.ts +17 -4
- package/lib/common/skill.d.ts.map +1 -1
- package/lib/common/skill.js +22 -35
- package/lib/common/skill.js.map +1 -1
- package/lib/common/tool-invocation-registry.d.ts +7 -0
- package/lib/common/tool-invocation-registry.d.ts.map +1 -1
- package/lib/common/tool-invocation-registry.js +5 -0
- package/lib/common/tool-invocation-registry.js.map +1 -1
- package/lib/common/variable-service.d.ts +2 -2
- package/lib/common/variable-service.d.ts.map +1 -1
- package/lib/common/variable-service.js +7 -4
- package/lib/common/variable-service.js.map +1 -1
- package/lib/common/variable-service.spec.js +2 -1
- package/lib/common/variable-service.spec.js.map +1 -1
- package/lib/node/backend-language-model-registry.d.ts.map +1 -1
- package/lib/node/backend-language-model-registry.js +1 -0
- package/lib/node/backend-language-model-registry.js.map +1 -1
- package/lib/node/backend-language-model-registry.spec.d.ts +2 -0
- package/lib/node/backend-language-model-registry.spec.d.ts.map +1 -0
- package/lib/node/backend-language-model-registry.spec.js +48 -0
- package/lib/node/backend-language-model-registry.spec.js.map +1 -0
- package/lib/node/token-usage-service-impl.d.ts +2 -0
- package/lib/node/token-usage-service-impl.d.ts.map +1 -1
- package/lib/node/token-usage-service-impl.js +7 -1
- package/lib/node/token-usage-service-impl.js.map +1 -1
- package/package.json +12 -12
- package/src/browser/{agent-completion-notification-service.ts → agent-notification-service.ts} +52 -27
- package/src/browser/ai-core-frontend-module.ts +2 -2
- package/src/browser/frontend-language-model-alias-registry.ts +3 -3
- package/src/browser/frontend-prompt-customization-service.spec.ts +319 -1
- package/src/browser/frontend-prompt-customization-service.ts +810 -59
- package/src/browser/frontend-variable-service.ts +5 -3
- package/src/browser/index.ts +1 -1
- package/src/browser/os-notification-service.ts +39 -24
- package/src/browser/prompt-variable-contribution.spec.ts +2 -1
- package/src/browser/skill-service.spec.ts +191 -38
- package/src/browser/skill-service.ts +61 -33
- package/src/browser/token-usage-frontend-service-impl.ts +6 -3
- package/src/browser/window-blink-service.ts +7 -4
- package/src/common/agent-preferences.ts +13 -2
- package/src/common/ai-core-preferences.ts +4 -2
- package/src/common/capability-variable-contribution.spec.ts +2 -1
- package/src/common/frontmatter.spec.ts +82 -0
- package/src/common/frontmatter.ts +99 -0
- package/src/common/index.ts +1 -0
- package/src/common/language-model-service.spec.ts +70 -0
- package/src/common/language-model-service.ts +1 -1
- package/src/common/language-model.ts +72 -3
- package/src/common/notification-types.ts +10 -0
- package/src/common/prompt-service-util.spec.ts +60 -0
- package/src/common/prompt-service-util.ts +31 -1
- package/src/common/prompt-service.spec.ts +59 -2
- package/src/common/prompt-service.ts +104 -8
- package/src/common/settings-service.ts +5 -0
- package/src/common/skill.ts +37 -39
- package/src/common/tool-invocation-registry.ts +13 -0
- package/src/common/variable-service.spec.ts +2 -2
- package/src/common/variable-service.ts +3 -1
- package/src/node/backend-language-model-registry.spec.ts +52 -0
- package/src/node/backend-language-model-registry.ts +1 -0
- package/src/node/token-usage-service-impl.ts +7 -2
- package/lib/browser/agent-completion-notification-service.d.ts.map +0 -1
- package/lib/browser/agent-completion-notification-service.js.map +0 -1
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { Disposable, MessageService, nls, Prioritizeable } from '@theia/core';
|
|
17
|
+
import { Disposable, MessageService, nls, Prioritizeable, ILogger } from '@theia/core';
|
|
18
18
|
import { FrontendApplicationContribution, OpenerService, open } from '@theia/core/lib/browser';
|
|
19
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
19
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
20
20
|
import {
|
|
21
21
|
AIVariable,
|
|
22
22
|
AIVariableArg,
|
|
@@ -108,6 +108,8 @@ export class DefaultFrontendVariableService extends DefaultAIVariableService imp
|
|
|
108
108
|
@inject(MessageService) protected readonly messageService: MessageService;
|
|
109
109
|
@inject(AIVariableResourceResolver) protected readonly aiResourceResolver: AIVariableResourceResolver;
|
|
110
110
|
@inject(OpenerService) protected readonly openerService: OpenerService;
|
|
111
|
+
@inject(ILogger) @named('ai-core:DefaultFrontendVariableService')
|
|
112
|
+
protected override readonly logger: ILogger;
|
|
111
113
|
|
|
112
114
|
onStart(): void {
|
|
113
115
|
this.initContributions();
|
|
@@ -199,7 +201,7 @@ export class DefaultFrontendVariableService extends DefaultAIVariableService imp
|
|
|
199
201
|
try {
|
|
200
202
|
return opener ? opener.open({ variable, arg }, context ?? {}) : this.openReadonly({ variable, arg }, context);
|
|
201
203
|
} catch (err) {
|
|
202
|
-
|
|
204
|
+
this.logger.error('Unable to open variable:', err);
|
|
203
205
|
this.messageService.error(nls.localize('theia/ai/core/unableToDisplayVariableValue', 'Unable to display variable value.'));
|
|
204
206
|
}
|
|
205
207
|
}
|
package/src/browser/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
export * from './agent-
|
|
17
|
+
export * from './agent-notification-service';
|
|
18
18
|
export * from './os-notification-service';
|
|
19
19
|
export * from './window-blink-service';
|
|
20
20
|
export * from './ai-activation-service';
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { injectable } from '@theia/core/shared/inversify';
|
|
17
|
+
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
|
18
18
|
import { nls } from '@theia/core/lib/common/nls';
|
|
19
|
-
import { environment } from '@theia/core';
|
|
19
|
+
import { environment, ILogger } from '@theia/core';
|
|
20
|
+
import { AgentNotificationKind, AGENT_NOTIFICATION_KIND_INPUT_NEEDED } from '../common/notification-types';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Configuration options for OS notifications
|
|
@@ -55,6 +56,9 @@ export interface OSNotificationResult {
|
|
|
55
56
|
@injectable()
|
|
56
57
|
export class OSNotificationService {
|
|
57
58
|
|
|
59
|
+
@inject(ILogger) @named('ai-core:OSNotificationService')
|
|
60
|
+
protected readonly logger: ILogger;
|
|
61
|
+
|
|
58
62
|
private isElectron: boolean;
|
|
59
63
|
|
|
60
64
|
constructor() {
|
|
@@ -96,7 +100,7 @@ export class OSNotificationService {
|
|
|
96
100
|
};
|
|
97
101
|
|
|
98
102
|
} catch (error) {
|
|
99
|
-
|
|
103
|
+
this.logger.error('Failed to show OS notification:', error);
|
|
100
104
|
return {
|
|
101
105
|
success: false,
|
|
102
106
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
@@ -134,7 +138,7 @@ export class OSNotificationService {
|
|
|
134
138
|
const permission = await Notification.requestPermission();
|
|
135
139
|
return permission;
|
|
136
140
|
} catch (error) {
|
|
137
|
-
|
|
141
|
+
this.logger.error('Failed to request notification permission:', error);
|
|
138
142
|
return 'denied';
|
|
139
143
|
}
|
|
140
144
|
}
|
|
@@ -149,33 +153,44 @@ export class OSNotificationService {
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
152
|
-
* Show a notification
|
|
153
|
-
* This is a convenience method with pre-configured options for agent notifications
|
|
156
|
+
* Show a notification for an agent event (task completed or input needed).
|
|
157
|
+
* This is a convenience method with pre-configured options for agent notifications.
|
|
154
158
|
*
|
|
155
|
-
* @param agentName The name of the agent
|
|
159
|
+
* @param agentName The name of the agent
|
|
160
|
+
* @param kind Whether the agent completed its task or needs user input
|
|
156
161
|
* @param sessionTitle Optional title of the chat session for identification
|
|
157
162
|
* @param onClickCallback Optional callback to invoke when the notification is clicked
|
|
158
163
|
* @returns Promise resolving to the notification result
|
|
159
164
|
*/
|
|
160
|
-
async
|
|
165
|
+
async showAgentNotification(
|
|
161
166
|
agentName: string,
|
|
167
|
+
kind: AgentNotificationKind,
|
|
162
168
|
sessionTitle?: string,
|
|
163
169
|
onClickCallback?: () => void,
|
|
164
170
|
): Promise<OSNotificationResult> {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
? nls.localize('theia/ai-core/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
const inputNeeded = kind === AGENT_NOTIFICATION_KIND_INPUT_NEEDED;
|
|
172
|
+
const title = inputNeeded
|
|
173
|
+
? nls.localize('theia/ai-core/agentInputNeededTitle', 'Agent "{0}" Needs Your Input', agentName)
|
|
174
|
+
: nls.localize('theia/ai-core/agentCompletionTitle', 'Agent "{0}" Task Completed', agentName);
|
|
175
|
+
const body = inputNeeded
|
|
176
|
+
? (sessionTitle
|
|
177
|
+
? nls.localize('theia/ai-core/agentInputNeededMessageWithSession',
|
|
178
|
+
'Agent "{0}" needs your input in "{1}".', agentName, sessionTitle)
|
|
179
|
+
: nls.localize('theia/ai-core/agentInputNeededMessage',
|
|
180
|
+
'Agent "{0}" needs your input.', agentName))
|
|
181
|
+
: (sessionTitle
|
|
182
|
+
? nls.localize('theia/ai-core/agentCompletionMessageWithSession',
|
|
183
|
+
'Agent "{0}" has completed its task in "{1}".', agentName, sessionTitle)
|
|
184
|
+
: nls.localize('theia/ai-core/agentCompletionMessage',
|
|
185
|
+
'Agent "{0}" has completed its task.', agentName));
|
|
171
186
|
|
|
172
187
|
return this.showNotification(title, {
|
|
173
188
|
body,
|
|
174
|
-
icon: this.
|
|
175
|
-
tag: `agent
|
|
189
|
+
icon: this.getAgentNotificationIcon(),
|
|
190
|
+
tag: `agent-${kind}-${agentName}-${sessionTitle ?? 'default'}`,
|
|
176
191
|
requireInteraction: false,
|
|
177
192
|
data: {
|
|
178
|
-
type:
|
|
193
|
+
type: `agent-${kind}`,
|
|
179
194
|
agentName,
|
|
180
195
|
sessionTitle,
|
|
181
196
|
timestamp: Date.now()
|
|
@@ -229,16 +244,16 @@ export class OSNotificationService {
|
|
|
229
244
|
const notification = new Notification(title, notificationOptions);
|
|
230
245
|
|
|
231
246
|
notification.onshow = () => {
|
|
232
|
-
|
|
247
|
+
this.logger.debug('OS notification shown:', title);
|
|
233
248
|
};
|
|
234
249
|
|
|
235
250
|
notification.onerror = error => {
|
|
236
|
-
|
|
251
|
+
this.logger.error('OS notification error:', error);
|
|
237
252
|
reject(new Error('Failed to show notification'));
|
|
238
253
|
};
|
|
239
254
|
|
|
240
255
|
notification.onclick = () => {
|
|
241
|
-
|
|
256
|
+
this.logger.debug('OS notification clicked:', title);
|
|
242
257
|
this.focusApplicationWindow();
|
|
243
258
|
if (onClickCallback) {
|
|
244
259
|
onClickCallback();
|
|
@@ -247,7 +262,7 @@ export class OSNotificationService {
|
|
|
247
262
|
};
|
|
248
263
|
|
|
249
264
|
notification.onclose = () => {
|
|
250
|
-
|
|
265
|
+
this.logger.debug('OS notification closed:', title);
|
|
251
266
|
};
|
|
252
267
|
|
|
253
268
|
resolve(notification);
|
|
@@ -271,16 +286,16 @@ export class OSNotificationService {
|
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
} catch (error) {
|
|
274
|
-
|
|
289
|
+
this.logger.debug('Could not focus application window:', error);
|
|
275
290
|
}
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
/**
|
|
279
|
-
* Get the icon URL for agent
|
|
294
|
+
* Get the icon URL for agent notifications
|
|
280
295
|
*
|
|
281
296
|
* @returns The icon URL or undefined if not available
|
|
282
297
|
*/
|
|
283
|
-
private
|
|
298
|
+
private getAgentNotificationIcon(): string | undefined {
|
|
284
299
|
// This could return a path to an icon file
|
|
285
300
|
// For now, we'll return undefined to use the default system icon
|
|
286
301
|
return undefined;
|
|
@@ -44,7 +44,8 @@ describe('PromptVariableContribution', () => {
|
|
|
44
44
|
// Set up PromptService
|
|
45
45
|
container.bind<PromptService>(PromptService).to(PromptServiceImpl).inSingletonScope();
|
|
46
46
|
const logger = sinon.createStubInstance(Logger);
|
|
47
|
-
const variableService = new DefaultAIVariableService({ getContributions: () => [] }
|
|
47
|
+
const variableService = new DefaultAIVariableService({ getContributions: () => [] });
|
|
48
|
+
(variableService as unknown as Record<string, unknown>)['logger'] = logger;
|
|
48
49
|
container.bind<AIVariableService>(AIVariableService).toConstantValue(variableService);
|
|
49
50
|
container.bind<ILogger>(ILogger).toConstantValue(new MockLogger);
|
|
50
51
|
|
|
@@ -55,94 +55,127 @@ describe('SkillService', () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
describe('directory prioritization', () => {
|
|
58
|
-
it('workspace
|
|
58
|
+
it('workspace directories come first when all directories provided', () => {
|
|
59
59
|
const result = combineSkillDirectories(
|
|
60
|
-
'/workspace/.prompts/skills',
|
|
60
|
+
['/workspace/.prompts/skills', '/workspace/.agents/skills'],
|
|
61
61
|
['/custom/skills1', '/custom/skills2'],
|
|
62
|
-
'/home/user/.theia/skills'
|
|
62
|
+
['/home/user/.theia/skills', '/home/user/.agents/skills']
|
|
63
63
|
);
|
|
64
64
|
|
|
65
65
|
expect(result).to.deep.equal([
|
|
66
|
+
{ path: '/workspace/.prompts/skills', tier: 'workspace' },
|
|
67
|
+
{ path: '/workspace/.agents/skills', tier: 'workspace' },
|
|
68
|
+
{ path: '/custom/skills1', tier: 'configured' },
|
|
69
|
+
{ path: '/custom/skills2', tier: 'configured' },
|
|
70
|
+
{ path: '/home/user/.theia/skills', tier: 'default' },
|
|
71
|
+
{ path: '/home/user/.agents/skills', tier: 'default' }
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('preserves the workspace-tier order: .prompts/skills wins over .agents/skills on duplicate skill names', () => {
|
|
76
|
+
// The `.prompts/skills` directory is intentionally listed first within the workspace tier
|
|
77
|
+
// because it is the Theia-explicit folder; users opting into it signal stronger intent than
|
|
78
|
+
// those relying on the generic `.agents/skills` convention.
|
|
79
|
+
const result = combineSkillDirectories(
|
|
80
|
+
['/workspace/.prompts/skills', '/workspace/.agents/skills'],
|
|
81
|
+
[],
|
|
82
|
+
[]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result.map(entry => entry.path)).to.deep.equal([
|
|
66
86
|
'/workspace/.prompts/skills',
|
|
67
|
-
'/
|
|
68
|
-
'/custom/skills2',
|
|
69
|
-
'/home/user/.theia/skills'
|
|
87
|
+
'/workspace/.agents/skills'
|
|
70
88
|
]);
|
|
71
89
|
});
|
|
72
90
|
|
|
73
|
-
it('works without workspace
|
|
91
|
+
it('works without workspace directories', () => {
|
|
74
92
|
const result = combineSkillDirectories(
|
|
75
|
-
|
|
93
|
+
[],
|
|
76
94
|
['/custom/skills'],
|
|
77
|
-
'/home/user/.theia/skills'
|
|
95
|
+
['/home/user/.theia/skills']
|
|
78
96
|
);
|
|
79
97
|
|
|
80
98
|
expect(result).to.deep.equal([
|
|
81
|
-
'/custom/skills',
|
|
82
|
-
'/home/user/.theia/skills'
|
|
99
|
+
{ path: '/custom/skills', tier: 'configured' },
|
|
100
|
+
{ path: '/home/user/.theia/skills', tier: 'default' }
|
|
83
101
|
]);
|
|
84
102
|
});
|
|
85
103
|
|
|
86
|
-
it('works with only default
|
|
104
|
+
it('works with only default directories', () => {
|
|
87
105
|
const result = combineSkillDirectories(
|
|
88
|
-
undefined,
|
|
89
106
|
[],
|
|
90
|
-
|
|
107
|
+
[],
|
|
108
|
+
['/home/user/.theia/skills', '/home/user/.agents/skills']
|
|
91
109
|
);
|
|
92
110
|
|
|
93
|
-
expect(result).to.deep.equal([
|
|
111
|
+
expect(result).to.deep.equal([
|
|
112
|
+
{ path: '/home/user/.theia/skills', tier: 'default' },
|
|
113
|
+
{ path: '/home/user/.agents/skills', tier: 'default' }
|
|
114
|
+
]);
|
|
94
115
|
});
|
|
95
116
|
|
|
96
117
|
it('deduplicates workspace directory if also in configured', () => {
|
|
97
118
|
const result = combineSkillDirectories(
|
|
98
|
-
'/workspace/.prompts/skills',
|
|
119
|
+
['/workspace/.prompts/skills'],
|
|
99
120
|
['/workspace/.prompts/skills', '/custom/skills'],
|
|
100
|
-
'/home/user/.theia/skills'
|
|
121
|
+
['/home/user/.theia/skills']
|
|
101
122
|
);
|
|
102
123
|
|
|
103
124
|
expect(result).to.deep.equal([
|
|
104
|
-
'/workspace/.prompts/skills',
|
|
105
|
-
'/custom/skills',
|
|
106
|
-
'/home/user/.theia/skills'
|
|
125
|
+
{ path: '/workspace/.prompts/skills', tier: 'workspace' },
|
|
126
|
+
{ path: '/custom/skills', tier: 'configured' },
|
|
127
|
+
{ path: '/home/user/.theia/skills', tier: 'default' }
|
|
107
128
|
]);
|
|
108
129
|
});
|
|
109
130
|
|
|
110
131
|
it('deduplicates default directory if also in configured', () => {
|
|
111
132
|
const result = combineSkillDirectories(
|
|
112
|
-
'/workspace/.prompts/skills',
|
|
133
|
+
['/workspace/.prompts/skills'],
|
|
113
134
|
['/home/user/.theia/skills'],
|
|
114
|
-
'/home/user/.theia/skills'
|
|
135
|
+
['/home/user/.theia/skills']
|
|
115
136
|
);
|
|
116
137
|
|
|
117
138
|
expect(result).to.deep.equal([
|
|
118
|
-
'/workspace/.prompts/skills',
|
|
119
|
-
'/home/user/.theia/skills'
|
|
139
|
+
{ path: '/workspace/.prompts/skills', tier: 'workspace' },
|
|
140
|
+
{ path: '/home/user/.theia/skills', tier: 'configured' }
|
|
120
141
|
]);
|
|
121
142
|
});
|
|
122
143
|
|
|
123
144
|
it('handles empty configured directories', () => {
|
|
124
145
|
const result = combineSkillDirectories(
|
|
125
|
-
'/workspace/.prompts/skills',
|
|
146
|
+
['/workspace/.prompts/skills'],
|
|
126
147
|
[],
|
|
127
|
-
'/home/user/.theia/skills'
|
|
148
|
+
['/home/user/.theia/skills']
|
|
128
149
|
);
|
|
129
150
|
|
|
130
151
|
expect(result).to.deep.equal([
|
|
131
|
-
'/workspace/.prompts/skills',
|
|
132
|
-
'/home/user/.theia/skills'
|
|
152
|
+
{ path: '/workspace/.prompts/skills', tier: 'workspace' },
|
|
153
|
+
{ path: '/home/user/.theia/skills', tier: 'default' }
|
|
133
154
|
]);
|
|
134
155
|
});
|
|
135
156
|
|
|
136
|
-
it('handles
|
|
157
|
+
it('handles empty default directories', () => {
|
|
137
158
|
const result = combineSkillDirectories(
|
|
138
|
-
'/workspace/.prompts/skills',
|
|
159
|
+
['/workspace/.prompts/skills'],
|
|
139
160
|
['/custom/skills'],
|
|
140
|
-
|
|
161
|
+
[]
|
|
141
162
|
);
|
|
142
163
|
|
|
143
164
|
expect(result).to.deep.equal([
|
|
144
|
-
'/workspace/.prompts/skills',
|
|
145
|
-
'/custom/skills'
|
|
165
|
+
{ path: '/workspace/.prompts/skills', tier: 'workspace' },
|
|
166
|
+
{ path: '/custom/skills', tier: 'configured' }
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('deduplicates duplicate workspace directories', () => {
|
|
171
|
+
const result = combineSkillDirectories(
|
|
172
|
+
['/workspace/.prompts/skills', '/workspace/.prompts/skills'],
|
|
173
|
+
[],
|
|
174
|
+
[]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result).to.deep.equal([
|
|
178
|
+
{ path: '/workspace/.prompts/skills', tier: 'workspace' }
|
|
146
179
|
]);
|
|
147
180
|
});
|
|
148
181
|
});
|
|
@@ -278,6 +311,7 @@ description: Skill with no content
|
|
|
278
311
|
let fileServiceMock: any;
|
|
279
312
|
let loggerWarnSpy: sinon.SinonStub;
|
|
280
313
|
let loggerInfoSpy: sinon.SinonStub;
|
|
314
|
+
let loggerDebugSpy: sinon.SinonStub;
|
|
281
315
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
282
316
|
let envVariablesServerMock: any;
|
|
283
317
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -295,6 +329,7 @@ description: Skill with no content
|
|
|
295
329
|
const loggerMock: ILogger = sinon.createStubInstance(Logger);
|
|
296
330
|
loggerMock.warn = loggerWarnSpy;
|
|
297
331
|
loggerMock.info = loggerInfoSpy;
|
|
332
|
+
loggerMock.debug = loggerDebugSpy;
|
|
298
333
|
(service as unknown as { logger: unknown }).logger = loggerMock;
|
|
299
334
|
(service as unknown as { envVariablesServer: unknown }).envVariablesServer = envVariablesServerMock;
|
|
300
335
|
(service as unknown as { workspaceService: unknown }).workspaceService = workspaceServiceMock;
|
|
@@ -315,6 +350,7 @@ description: Skill with no content
|
|
|
315
350
|
|
|
316
351
|
loggerWarnSpy = sinon.stub();
|
|
317
352
|
loggerInfoSpy = sinon.stub();
|
|
353
|
+
loggerDebugSpy = sinon.stub();
|
|
318
354
|
|
|
319
355
|
envVariablesServerMock = {
|
|
320
356
|
getHomeDirUri: sinon.stub().resolves('file:///home/testuser'),
|
|
@@ -362,13 +398,17 @@ description: Skill with no content
|
|
|
362
398
|
sinon.match({ recursive: false, excludes: [] })
|
|
363
399
|
)).to.be.true;
|
|
364
400
|
|
|
365
|
-
// Verify
|
|
366
|
-
expect(
|
|
401
|
+
// Verify debug log about watching parent (demoted from info to keep startup quiet)
|
|
402
|
+
expect(loggerDebugSpy.calledWith(
|
|
367
403
|
sinon.match(/Watching parent directory.*for skills folder creation/)
|
|
368
404
|
)).to.be.true;
|
|
405
|
+
// And the same line must not be emitted at info level any more.
|
|
406
|
+
expect(loggerInfoSpy.calledWith(
|
|
407
|
+
sinon.match(/Watching parent directory/)
|
|
408
|
+
)).to.be.false;
|
|
369
409
|
});
|
|
370
410
|
|
|
371
|
-
it('should log
|
|
411
|
+
it('should log a debug entry when parent directory does not exist', async () => {
|
|
372
412
|
const service = createService();
|
|
373
413
|
|
|
374
414
|
// Neither skills directory nor parent exists
|
|
@@ -379,10 +419,42 @@ description: Skill with no content
|
|
|
379
419
|
await workspaceServiceMock.ready;
|
|
380
420
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
381
421
|
|
|
382
|
-
//
|
|
383
|
-
expect(
|
|
422
|
+
// The absent-default-tree case is expected on fresh machines and is now debug-level only.
|
|
423
|
+
expect(loggerDebugSpy.calledWith(
|
|
384
424
|
sinon.match(/Cannot watch skills directory.*parent directory does not exist/)
|
|
385
425
|
)).to.be.true;
|
|
426
|
+
// It must no longer surface as a startup warning.
|
|
427
|
+
expect(loggerWarnSpy.calledWith(
|
|
428
|
+
sinon.match(/Cannot watch skills directory.*parent directory does not exist/)
|
|
429
|
+
)).to.be.false;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('coalesces concurrent update() calls so a single scan runs at a time', async () => {
|
|
433
|
+
const service = createService();
|
|
434
|
+
|
|
435
|
+
// Default skills directory does not exist, but parent does, so each update logs once.
|
|
436
|
+
fileServiceMock.exists
|
|
437
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.theia-ide/skills'))
|
|
438
|
+
.resolves(false);
|
|
439
|
+
fileServiceMock.exists
|
|
440
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.theia-ide'))
|
|
441
|
+
.resolves(true);
|
|
442
|
+
|
|
443
|
+
// Drive two updates in the same tick - the second must be coalesced into a follow-up
|
|
444
|
+
// re-schedule rather than racing the first and producing duplicated log lines.
|
|
445
|
+
const update = (service as unknown as { update: () => Promise<void> }).update.bind(service);
|
|
446
|
+
await Promise.all([update(), update()]);
|
|
447
|
+
// Give the rescheduled run (debounced via scheduleUpdate) time to fire.
|
|
448
|
+
await new Promise(resolve => setTimeout(resolve, 80));
|
|
449
|
+
|
|
450
|
+
// The watcher must be installed (the underlying scan must have run); the same log line
|
|
451
|
+
// must have been emitted at most once per scan, never twice from interleaved scans.
|
|
452
|
+
const matchingDebugCalls = loggerDebugSpy.getCalls().filter(call =>
|
|
453
|
+
/Watching parent directory.*for skills folder creation/.test(String(call.args[0]))
|
|
454
|
+
);
|
|
455
|
+
// Two scans (initial + rescheduled) are allowed, but never more, and never zero.
|
|
456
|
+
expect(matchingDebugCalls.length).to.be.greaterThan(0);
|
|
457
|
+
expect(matchingDebugCalls.length).to.be.lessThan(3);
|
|
386
458
|
});
|
|
387
459
|
|
|
388
460
|
it('should log warning for non-existent configured directories', async () => {
|
|
@@ -477,5 +549,86 @@ Test skill content`
|
|
|
477
549
|
expect(skills).to.have.length(1);
|
|
478
550
|
expect(skills[0].name).to.equal('test-skill');
|
|
479
551
|
});
|
|
552
|
+
|
|
553
|
+
it('prefers a workspace `.prompts/skills` skill over a same-named `.agents/skills` skill in the same root', async () => {
|
|
554
|
+
const service = createService();
|
|
555
|
+
|
|
556
|
+
workspaceServiceMock.tryGetRoots.returns([
|
|
557
|
+
{ resource: URI.fromFilePath('/workspace') }
|
|
558
|
+
]);
|
|
559
|
+
|
|
560
|
+
// Stub default skill directories as non-existent to keep focus on workspace behavior.
|
|
561
|
+
fileServiceMock.exists
|
|
562
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.theia-ide/skills'))
|
|
563
|
+
.resolves(false);
|
|
564
|
+
fileServiceMock.exists
|
|
565
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.theia-ide'))
|
|
566
|
+
.resolves(false);
|
|
567
|
+
fileServiceMock.exists
|
|
568
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.agents/skills'))
|
|
569
|
+
.resolves(false);
|
|
570
|
+
fileServiceMock.exists
|
|
571
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.agents'))
|
|
572
|
+
.resolves(false);
|
|
573
|
+
|
|
574
|
+
// Both workspace skill directories exist and contain a `dup-skill` directory.
|
|
575
|
+
const promptsSkillDir = '/workspace/.prompts/skills';
|
|
576
|
+
const agentsSkillDir = '/workspace/.agents/skills';
|
|
577
|
+
fileServiceMock.exists
|
|
578
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === promptsSkillDir))
|
|
579
|
+
.resolves(true);
|
|
580
|
+
fileServiceMock.exists
|
|
581
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === agentsSkillDir))
|
|
582
|
+
.resolves(true);
|
|
583
|
+
fileServiceMock.resolve
|
|
584
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === promptsSkillDir))
|
|
585
|
+
.resolves({
|
|
586
|
+
children: [{
|
|
587
|
+
isDirectory: true,
|
|
588
|
+
name: 'dup-skill',
|
|
589
|
+
resource: URI.fromFilePath(`${promptsSkillDir}/dup-skill`)
|
|
590
|
+
}]
|
|
591
|
+
});
|
|
592
|
+
fileServiceMock.resolve
|
|
593
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === agentsSkillDir))
|
|
594
|
+
.resolves({
|
|
595
|
+
children: [{
|
|
596
|
+
isDirectory: true,
|
|
597
|
+
name: 'dup-skill',
|
|
598
|
+
resource: URI.fromFilePath(`${agentsSkillDir}/dup-skill`)
|
|
599
|
+
}]
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Each tier provides its own SKILL.md so we can distinguish them by location.
|
|
603
|
+
fileServiceMock.exists
|
|
604
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === `${promptsSkillDir}/dup-skill/SKILL.md`))
|
|
605
|
+
.resolves(true);
|
|
606
|
+
fileServiceMock.exists
|
|
607
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === `${agentsSkillDir}/dup-skill/SKILL.md`))
|
|
608
|
+
.resolves(true);
|
|
609
|
+
fileServiceMock.read
|
|
610
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === `${promptsSkillDir}/dup-skill/SKILL.md`))
|
|
611
|
+
.resolves({
|
|
612
|
+
value: '---\nname: dup-skill\ndescription: from prompts\n---\nFrom prompts'
|
|
613
|
+
});
|
|
614
|
+
fileServiceMock.read
|
|
615
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === `${agentsSkillDir}/dup-skill/SKILL.md`))
|
|
616
|
+
.resolves({
|
|
617
|
+
value: '---\nname: dup-skill\ndescription: from agents\n---\nFrom agents'
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
(service as unknown as { init: () => void }).init();
|
|
621
|
+
await workspaceServiceMock.ready;
|
|
622
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
623
|
+
|
|
624
|
+
const skill = service.getSkill('dup-skill');
|
|
625
|
+
expect(skill, 'dup-skill should be discovered').to.not.be.undefined;
|
|
626
|
+
expect(skill!.description).to.equal('from prompts');
|
|
627
|
+
expect(skill!.location).to.equal(new Path(`${promptsSkillDir}/dup-skill/SKILL.md`).fsPath());
|
|
628
|
+
// The second occurrence is reported (not silently swallowed) via a warning.
|
|
629
|
+
expect(loggerWarnSpy.calledWith(
|
|
630
|
+
sinon.match(/Duplicate skill found.*dup-skill/)
|
|
631
|
+
)).to.be.true;
|
|
632
|
+
});
|
|
480
633
|
});
|
|
481
634
|
});
|