@theia/ai-core 1.75.0-next.8 → 1.75.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/lib/browser/agent-plugin-ui-bridge.d.ts +26 -0
- package/lib/browser/agent-plugin-ui-bridge.d.ts.map +1 -0
- package/lib/browser/agent-plugin-ui-bridge.js +20 -0
- package/lib/browser/agent-plugin-ui-bridge.js.map +1 -0
- package/lib/browser/ai-core-command-contribution.d.ts.map +1 -1
- package/lib/browser/ai-core-command-contribution.js +4 -1
- package/lib/browser/ai-core-command-contribution.js.map +1 -1
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +6 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-settings-service.js +1 -0
- package/lib/browser/ai-settings-service.js.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.js +1 -1
- package/lib/browser/frontend-language-model-registry.d.ts +2 -0
- package/lib/browser/frontend-language-model-registry.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-registry.js +22 -1
- package/lib/browser/frontend-language-model-registry.js.map +1 -1
- package/lib/browser/generic-capabilities-variable-contribution.d.ts.map +1 -1
- package/lib/browser/generic-capabilities-variable-contribution.js +4 -1
- package/lib/browser/generic-capabilities-variable-contribution.js.map +1 -1
- package/lib/browser/hide-ai-preferences-contribution.d.ts +30 -0
- package/lib/browser/hide-ai-preferences-contribution.d.ts.map +1 -0
- package/lib/browser/hide-ai-preferences-contribution.js +90 -0
- package/lib/browser/hide-ai-preferences-contribution.js.map +1 -0
- package/lib/browser/hide-ai-preferences-contribution.spec.d.ts +2 -0
- package/lib/browser/hide-ai-preferences-contribution.spec.d.ts.map +1 -0
- package/lib/browser/hide-ai-preferences-contribution.spec.js +95 -0
- package/lib/browser/hide-ai-preferences-contribution.spec.js.map +1 -0
- package/lib/browser/index.d.ts +3 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +3 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/prompt-variable-contribution.d.ts +1 -1
- package/lib/browser/prompt-variable-contribution.d.ts.map +1 -1
- package/lib/browser/prompt-variable-contribution.js +1 -0
- package/lib/browser/prompt-variable-contribution.js.map +1 -1
- package/lib/browser/skill-prompt-coordinator.d.ts +9 -1
- package/lib/browser/skill-prompt-coordinator.d.ts.map +1 -1
- package/lib/browser/skill-prompt-coordinator.js +29 -14
- package/lib/browser/skill-prompt-coordinator.js.map +1 -1
- package/lib/browser/skill-prompt-coordinator.spec.d.ts +2 -0
- package/lib/browser/skill-prompt-coordinator.spec.d.ts.map +1 -0
- package/lib/browser/skill-prompt-coordinator.spec.js +98 -0
- package/lib/browser/skill-prompt-coordinator.spec.js.map +1 -0
- package/lib/browser/skill-registry-ui-bridge.d.ts +25 -0
- package/lib/browser/skill-registry-ui-bridge.d.ts.map +1 -0
- package/lib/browser/skill-registry-ui-bridge.js +20 -0
- package/lib/browser/skill-registry-ui-bridge.js.map +1 -0
- package/lib/browser/skill-service.d.ts +33 -8
- package/lib/browser/skill-service.d.ts.map +1 -1
- package/lib/browser/skill-service.js +95 -39
- package/lib/browser/skill-service.js.map +1 -1
- package/lib/browser/skill-service.spec.js +181 -2
- package/lib/browser/skill-service.spec.js.map +1 -1
- package/lib/browser/skills-variable-contribution.d.ts +4 -0
- package/lib/browser/skills-variable-contribution.d.ts.map +1 -1
- package/lib/browser/skills-variable-contribution.js +10 -1
- package/lib/browser/skills-variable-contribution.js.map +1 -1
- package/lib/browser/skills-variable-contribution.spec.js +6 -0
- package/lib/browser/skills-variable-contribution.spec.js.map +1 -1
- package/lib/browser/theia-variable-contribution.js +6 -12
- package/lib/browser/theia-variable-contribution.js.map +1 -1
- package/lib/common/ai-core-preferences.d.ts +17 -0
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +9 -1
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/ai-variable-resource.d.ts +2 -1
- package/lib/common/ai-variable-resource.d.ts.map +1 -1
- package/lib/common/ai-variable-resource.js +6 -1
- package/lib/common/ai-variable-resource.js.map +1 -1
- package/lib/common/capability-variable-contribution.d.ts +1 -1
- package/lib/common/capability-variable-contribution.d.ts.map +1 -1
- package/lib/common/capability-variable-contribution.js +1 -0
- package/lib/common/capability-variable-contribution.js.map +1 -1
- package/lib/common/language-model.d.ts +16 -2
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +39 -3
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/language-model.spec.js +75 -0
- package/lib/common/language-model.spec.js.map +1 -1
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +7 -4
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +36 -0
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/skill.d.ts +31 -7
- package/lib/common/skill.d.ts.map +1 -1
- package/lib/common/skill.js +37 -14
- package/lib/common/skill.js.map +1 -1
- package/lib/common/skill.spec.js +70 -0
- package/lib/common/skill.spec.js.map +1 -1
- package/lib/common/today-variable-contribution.d.ts.map +1 -1
- package/lib/common/today-variable-contribution.js +1 -1
- package/lib/common/today-variable-contribution.js.map +1 -1
- package/lib/common/tool-invocation-registry.d.ts +2 -1
- package/lib/common/tool-invocation-registry.d.ts.map +1 -1
- package/lib/common/tool-invocation-registry.js +6 -1
- package/lib/common/tool-invocation-registry.js.map +1 -1
- package/lib/node/language-model-frontend-delegate.d.ts +2 -2
- package/lib/node/language-model-frontend-delegate.d.ts.map +1 -1
- package/lib/node/language-model-frontend-delegate.js +1 -0
- package/lib/node/language-model-frontend-delegate.js.map +1 -1
- package/package.json +12 -12
- package/src/browser/agent-plugin-ui-bridge.ts +43 -0
- package/src/browser/ai-core-command-contribution.ts +7 -1
- package/src/browser/ai-core-frontend-module.ts +11 -2
- package/src/browser/ai-settings-service.ts +2 -2
- package/src/browser/frontend-language-model-alias-registry.ts +1 -1
- package/src/browser/frontend-language-model-registry.ts +6 -2
- package/src/browser/generic-capabilities-variable-contribution.ts +4 -1
- package/src/browser/hide-ai-preferences-contribution.spec.ts +117 -0
- package/src/browser/hide-ai-preferences-contribution.ts +88 -0
- package/src/browser/index.ts +3 -0
- package/src/browser/prompt-variable-contribution.ts +3 -3
- package/src/browser/skill-prompt-coordinator.spec.ts +114 -0
- package/src/browser/skill-prompt-coordinator.ts +29 -17
- package/src/browser/skill-registry-ui-bridge.ts +41 -0
- package/src/browser/skill-service.spec.ts +229 -4
- package/src/browser/skill-service.ts +120 -43
- package/src/browser/skills-variable-contribution.spec.ts +6 -0
- package/src/browser/skills-variable-contribution.ts +14 -4
- package/src/browser/theia-variable-contribution.ts +12 -12
- package/src/common/ai-core-preferences.ts +19 -0
- package/src/common/ai-variable-resource.ts +6 -3
- package/src/common/capability-variable-contribution.ts +3 -3
- package/src/common/language-model.spec.ts +83 -0
- package/src/common/language-model.ts +35 -5
- package/src/common/prompt-service.spec.ts +43 -0
- package/src/common/prompt-service.ts +8 -6
- package/src/common/skill.spec.ts +99 -0
- package/src/common/skill.ts +52 -17
- package/src/common/today-variable-contribution.ts +2 -1
- package/src/common/tool-invocation-registry.ts +5 -2
- package/src/node/language-model-frontend-delegate.ts +3 -3
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
17
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ILogger } from '@theia/core';
|
|
18
19
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
|
19
20
|
import { SkillService } from './skill-service';
|
|
20
21
|
import { PromptService } from '../common/prompt-service';
|
|
@@ -28,45 +29,56 @@ export class SkillPromptCoordinator implements FrontendApplicationContribution {
|
|
|
28
29
|
@inject(PromptService)
|
|
29
30
|
protected readonly promptService: PromptService;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
async onStart(): Promise<void> {
|
|
34
|
-
// Wait for skills to be loaded before registering commands
|
|
35
|
-
await this.skillService.ready;
|
|
32
|
+
@inject(ILogger) @named('ai-core:SkillPromptCoordinator')
|
|
33
|
+
protected readonly logger: ILogger;
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
this.updateSkillCommands();
|
|
35
|
+
protected registeredSkillCommands = new Set<string>();
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
this.skillService.
|
|
37
|
+
onStart(): void {
|
|
38
|
+
this.skillService.ready.then(() => {
|
|
39
|
+
this.skillService.onSkillsChanged(() => {
|
|
40
|
+
this.updateSkillCommands();
|
|
41
|
+
});
|
|
42
42
|
this.updateSkillCommands();
|
|
43
|
+
}).catch(error => {
|
|
44
|
+
this.logger.error('Failed to update skill commands', error);
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
protected updateSkillCommands(): void {
|
|
47
49
|
const currentSkills = this.skillService.getSkills();
|
|
48
|
-
|
|
50
|
+
// Keyed by qualified name: two skills owned by different plugins can share a plain name.
|
|
51
|
+
const currentSkillNames = new Set(currentSkills.map(s => s.qualifiedName));
|
|
49
52
|
|
|
50
53
|
// Unregister removed skills
|
|
51
54
|
for (const name of this.registeredSkillCommands) {
|
|
52
55
|
if (!currentSkillNames.has(name)) {
|
|
53
|
-
this.promptService.removePromptFragment(
|
|
56
|
+
this.promptService.removePromptFragment(this.fragmentId(name));
|
|
54
57
|
this.registeredSkillCommands.delete(name);
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
// Register new skills
|
|
59
62
|
for (const skill of currentSkills) {
|
|
60
|
-
if (!this.registeredSkillCommands.has(skill.
|
|
63
|
+
if (!this.registeredSkillCommands.has(skill.qualifiedName)) {
|
|
61
64
|
this.promptService.addBuiltInPromptFragment({
|
|
62
|
-
id:
|
|
63
|
-
template: `Load the skill ${skill.
|
|
65
|
+
id: this.fragmentId(skill.qualifiedName),
|
|
66
|
+
template: `Load the skill ${skill.qualifiedName} using ~{getSkillFileContent}.`,
|
|
64
67
|
isCommand: true,
|
|
65
|
-
commandName: skill.
|
|
68
|
+
commandName: skill.qualifiedName,
|
|
66
69
|
commandDescription: skill.description
|
|
67
70
|
});
|
|
68
|
-
this.registeredSkillCommands.add(skill.
|
|
71
|
+
this.registeredSkillCommands.add(skill.qualifiedName);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A fragment id becomes a file name when the user customizes the fragment, so the qualifier's `:`
|
|
78
|
+
* cannot survive into it - it is reserved on Windows. Only the id is sanitized; the command keeps
|
|
79
|
+
* the qualified name.
|
|
80
|
+
*/
|
|
81
|
+
protected fragmentId(qualifiedName: string): string {
|
|
82
|
+
return `skill-command-${qualifiedName.replace(/:/g, '__')}`;
|
|
83
|
+
}
|
|
72
84
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { Event } from '@theia/core';
|
|
18
|
+
import { Skill } from '../common/skill';
|
|
19
|
+
|
|
20
|
+
export const SkillRegistryUiBridge = Symbol('SkillRegistryUiBridge');
|
|
21
|
+
/**
|
|
22
|
+
* Optional integration point implemented by `@theia/ai-registry`, so that packages which must not
|
|
23
|
+
* depend on it can tell which skills it manages and reveal them in the registry. The counterpart of
|
|
24
|
+
* {@link AgentPluginUiBridge} for skills installed or linked directly, rather than contributed by a
|
|
25
|
+
* plugin.
|
|
26
|
+
*
|
|
27
|
+
* Nothing is bound by default. Consumers inject it `@optional()` and, when it is absent, hide every
|
|
28
|
+
* registry affordance - a product without `@theia/ai-registry` has no registry to install from.
|
|
29
|
+
*/
|
|
30
|
+
export interface SkillRegistryUiBridge {
|
|
31
|
+
/**
|
|
32
|
+
* The registry entry `skill` was installed from or linked to, or `undefined` for a skill the
|
|
33
|
+
* registry does not manage - a workspace skill, a configured directory, or one contributed by an
|
|
34
|
+
* Agent Plugin. Takes the whole skill rather than its name: a name alone cannot tell a managed
|
|
35
|
+
* skill from one of the same name in a directory the user controls.
|
|
36
|
+
*/
|
|
37
|
+
getRegistryEntryId(skill: Skill): string | undefined;
|
|
38
|
+
/** Reveals the entry in the registry UI. A no-op for a skill the registry does not manage. */
|
|
39
|
+
revealSkill(skillId: string): void;
|
|
40
|
+
readonly onDidChange: Event<void>;
|
|
41
|
+
}
|
|
@@ -18,15 +18,22 @@ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
|
18
18
|
const disableJSDOM = enableJSDOM();
|
|
19
19
|
|
|
20
20
|
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
21
|
-
|
|
21
|
+
// Guarded: another spec in the same mocha process may already have set it, and `set` throws on a
|
|
22
|
+
// second call.
|
|
23
|
+
try {
|
|
24
|
+
FrontendApplicationConfigProvider.get();
|
|
25
|
+
} catch {
|
|
26
|
+
FrontendApplicationConfigProvider.set({});
|
|
27
|
+
}
|
|
22
28
|
|
|
23
29
|
import { expect } from 'chai';
|
|
24
30
|
import * as sinon from 'sinon';
|
|
25
|
-
import { parseSkillFile, combineSkillDirectories } from '../common/skill';
|
|
31
|
+
import { parseSkillFile, combineSkillDirectories, Skill, SkillDirectoryEntry } from '../common/skill';
|
|
26
32
|
import { Path } from '@theia/core/lib/common/path';
|
|
27
33
|
import { Disposable, Emitter, ILogger, Logger, URI } from '@theia/core';
|
|
34
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
28
35
|
import { FileChangesEvent } from '@theia/filesystem/lib/common/files';
|
|
29
|
-
import { DefaultSkillService } from './skill-service';
|
|
36
|
+
import { DefaultSkillService, SkillDirectoryContribution } from './skill-service';
|
|
30
37
|
|
|
31
38
|
disableJSDOM();
|
|
32
39
|
|
|
@@ -322,8 +329,11 @@ description: Skill with no content
|
|
|
322
329
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
323
330
|
let preferenceChangedEmitter: Emitter<any>;
|
|
324
331
|
|
|
325
|
-
function createService(): DefaultSkillService {
|
|
332
|
+
function createService(contributions: SkillDirectoryContribution[] = []): DefaultSkillService {
|
|
326
333
|
const service = new DefaultSkillService();
|
|
334
|
+
(service as unknown as { skillDirectoryContributions: unknown }).skillDirectoryContributions = {
|
|
335
|
+
getContributions: () => contributions
|
|
336
|
+
};
|
|
327
337
|
(service as unknown as { preferences: unknown }).preferences = preferencesMock;
|
|
328
338
|
(service as unknown as { fileService: unknown }).fileService = fileServiceMock;
|
|
329
339
|
const loggerMock: ILogger = sinon.createStubInstance(Logger);
|
|
@@ -365,6 +375,7 @@ description: Skill with no content
|
|
|
365
375
|
|
|
366
376
|
preferencesMock = {
|
|
367
377
|
'ai-features.skills.skillDirectories': [],
|
|
378
|
+
ready: Promise.resolve(),
|
|
368
379
|
onPreferenceChanged: preferenceChangedEmitter.event
|
|
369
380
|
};
|
|
370
381
|
});
|
|
@@ -487,6 +498,41 @@ description: Skill with no content
|
|
|
487
498
|
)).to.be.true;
|
|
488
499
|
});
|
|
489
500
|
|
|
501
|
+
it('should scan configured directories that only become known once the preferences are ready', async () => {
|
|
502
|
+
// The preference proxy serves defaults until its `ready` promise resolves, so a scan started before
|
|
503
|
+
// that point misses the configured directories.
|
|
504
|
+
const preferencesReady = new Deferred<void>();
|
|
505
|
+
preferencesMock.ready = preferencesReady.promise;
|
|
506
|
+
|
|
507
|
+
// Default skills directory exists and is empty, so the configured directory is the only source of warnings.
|
|
508
|
+
fileServiceMock.exists
|
|
509
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.theia-ide/skills'))
|
|
510
|
+
.resolves(true);
|
|
511
|
+
fileServiceMock.resolve
|
|
512
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/home/testuser/.theia-ide/skills'))
|
|
513
|
+
.resolves({ children: [] });
|
|
514
|
+
fileServiceMock.exists
|
|
515
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === '/custom/configured/skills'))
|
|
516
|
+
.resolves(false);
|
|
517
|
+
|
|
518
|
+
const service = createService();
|
|
519
|
+
(service as unknown as { init: () => void }).init();
|
|
520
|
+
await workspaceServiceMock.ready;
|
|
521
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
522
|
+
|
|
523
|
+
// Nothing may be scanned yet, the preference value is still the default one.
|
|
524
|
+
expect(fileServiceMock.exists.called).to.be.false;
|
|
525
|
+
|
|
526
|
+
(preferencesMock as Record<string, unknown>)['ai-features.skills.skillDirectories'] = ['/custom/configured/skills'];
|
|
527
|
+
preferencesReady.resolve();
|
|
528
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
529
|
+
|
|
530
|
+
// The scan picked up the configured directory instead of the default preference value.
|
|
531
|
+
expect(loggerWarnSpy.calledWith(
|
|
532
|
+
sinon.match(/Configured skill directory '\/custom\/configured\/skills' does not exist/)
|
|
533
|
+
)).to.be.true;
|
|
534
|
+
});
|
|
535
|
+
|
|
490
536
|
it('should load skills when directory is created after initialization', async () => {
|
|
491
537
|
const service = createService();
|
|
492
538
|
|
|
@@ -630,5 +676,184 @@ Test skill content`
|
|
|
630
676
|
sinon.match(/Duplicate skill found.*dup-skill/)
|
|
631
677
|
)).to.be.true;
|
|
632
678
|
});
|
|
679
|
+
|
|
680
|
+
describe('contributed skill directories', () => {
|
|
681
|
+
|
|
682
|
+
const bigQueryQualifier = 'bigquery-data-analytics';
|
|
683
|
+
const athenaQualifier = 'athena-analytics';
|
|
684
|
+
|
|
685
|
+
/** A contribution returning a fixed set of roots. */
|
|
686
|
+
function contributing(...entries: SkillDirectoryEntry[]): SkillDirectoryContribution {
|
|
687
|
+
return { getSkillDirectories: () => entries };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Stubs `directory` as an existing skills root holding one skill directory per given name,
|
|
692
|
+
* each with a valid `SKILL.md` whose description names the root it came from.
|
|
693
|
+
*/
|
|
694
|
+
function stubSkillsRoot(directory: string, ...skillNames: string[]): void {
|
|
695
|
+
fileServiceMock.exists
|
|
696
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === directory))
|
|
697
|
+
.resolves(true);
|
|
698
|
+
fileServiceMock.resolve
|
|
699
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === directory))
|
|
700
|
+
.resolves({
|
|
701
|
+
children: skillNames.map(name => ({
|
|
702
|
+
isDirectory: true,
|
|
703
|
+
name,
|
|
704
|
+
resource: URI.fromFilePath(`${directory}/${name}`)
|
|
705
|
+
}))
|
|
706
|
+
});
|
|
707
|
+
for (const name of skillNames) {
|
|
708
|
+
const skillFile = `${directory}/${name}/SKILL.md`;
|
|
709
|
+
fileServiceMock.exists
|
|
710
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === skillFile))
|
|
711
|
+
.resolves(true);
|
|
712
|
+
fileServiceMock.read
|
|
713
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === skillFile))
|
|
714
|
+
.resolves({ value: `---\nname: ${name}\ndescription: from ${directory}\n---\nBody of ${name}` });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function initialize(service: DefaultSkillService): Promise<void> {
|
|
719
|
+
(service as unknown as { init: () => void }).init();
|
|
720
|
+
await workspaceServiceMock.ready;
|
|
721
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
it('qualifies the names of skills found under an owned contributed root', async () => {
|
|
725
|
+
const pluginSkills = '/home/testuser/.agents/plugins/bigquery/skills';
|
|
726
|
+
stubSkillsRoot(pluginSkills, 'query-builder');
|
|
727
|
+
|
|
728
|
+
const service = createService([contributing({ path: pluginSkills, tier: 'plugin', qualifier: bigQueryQualifier })]);
|
|
729
|
+
await initialize(service);
|
|
730
|
+
|
|
731
|
+
const skills = service.getSkills();
|
|
732
|
+
expect(skills).to.have.length(1);
|
|
733
|
+
expect(skills[0].name).to.equal('query-builder');
|
|
734
|
+
expect(skills[0].qualifiedName).to.equal('bigquery-data-analytics:query-builder');
|
|
735
|
+
expect(Skill.qualifierOf(skills[0])).to.equal('bigquery-data-analytics');
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('leaves the qualified name of a skill from a built-in root equal to its plain name', async () => {
|
|
739
|
+
stubSkillsRoot('/home/testuser/.theia-ide/skills', 'code-review');
|
|
740
|
+
|
|
741
|
+
const service = createService();
|
|
742
|
+
await initialize(service);
|
|
743
|
+
|
|
744
|
+
const skills = service.getSkills();
|
|
745
|
+
expect(skills).to.have.length(1);
|
|
746
|
+
expect(skills[0].qualifiedName).to.equal('code-review');
|
|
747
|
+
expect(Skill.qualifierOf(skills[0])).to.be.undefined;
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('resolves an owned skill by its qualified name and by its plain name', async () => {
|
|
751
|
+
const pluginSkills = '/home/testuser/.agents/plugins/bigquery/skills';
|
|
752
|
+
stubSkillsRoot(pluginSkills, 'query-builder');
|
|
753
|
+
|
|
754
|
+
const service = createService([contributing({ path: pluginSkills, tier: 'plugin', qualifier: bigQueryQualifier })]);
|
|
755
|
+
await initialize(service);
|
|
756
|
+
|
|
757
|
+
expect(service.getSkill('bigquery-data-analytics:query-builder')?.name).to.equal('query-builder');
|
|
758
|
+
// The plain name keeps working, so every reference written before the skill was owned
|
|
759
|
+
// still resolves.
|
|
760
|
+
expect(service.getSkill('query-builder')?.qualifiedName).to.equal('bigquery-data-analytics:query-builder');
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('loads a skill of the same name from two different plugins and keeps both addressable', async () => {
|
|
764
|
+
const bigQuerySkills = '/home/testuser/.agents/plugins/bigquery/skills';
|
|
765
|
+
const athenaSkills = '/home/testuser/.agents/plugins/athena/skills';
|
|
766
|
+
stubSkillsRoot(bigQuerySkills, 'query-builder');
|
|
767
|
+
stubSkillsRoot(athenaSkills, 'query-builder');
|
|
768
|
+
|
|
769
|
+
const service = createService([contributing(
|
|
770
|
+
{ path: bigQuerySkills, tier: 'plugin', qualifier: bigQueryQualifier },
|
|
771
|
+
{ path: athenaSkills, tier: 'plugin', qualifier: athenaQualifier }
|
|
772
|
+
)]);
|
|
773
|
+
await initialize(service);
|
|
774
|
+
|
|
775
|
+
expect(service.getSkills()).to.have.length(2);
|
|
776
|
+
expect(service.getSkill('bigquery-data-analytics:query-builder')?.description).to.equal(`from ${bigQuerySkills}`);
|
|
777
|
+
expect(service.getSkill('athena-analytics:query-builder')?.description).to.equal(`from ${athenaSkills}`);
|
|
778
|
+
// Neither is dropped as a duplicate, because they no longer share a name.
|
|
779
|
+
expect(loggerWarnSpy.calledWith(sinon.match(/Duplicate skill found/))).to.be.false;
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('resolves an ambiguous plain name to undefined rather than guessing one of the candidates', async () => {
|
|
783
|
+
const bigQuerySkills = '/home/testuser/.agents/plugins/bigquery/skills';
|
|
784
|
+
const athenaSkills = '/home/testuser/.agents/plugins/athena/skills';
|
|
785
|
+
stubSkillsRoot(bigQuerySkills, 'query-builder');
|
|
786
|
+
stubSkillsRoot(athenaSkills, 'query-builder');
|
|
787
|
+
|
|
788
|
+
const service = createService([contributing(
|
|
789
|
+
{ path: bigQuerySkills, tier: 'plugin', qualifier: bigQueryQualifier },
|
|
790
|
+
{ path: athenaSkills, tier: 'plugin', qualifier: athenaQualifier }
|
|
791
|
+
)]);
|
|
792
|
+
await initialize(service);
|
|
793
|
+
|
|
794
|
+
expect(service.getSkill('query-builder')).to.be.undefined;
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('keeps loading the remaining roots when one contribution fails', async () => {
|
|
798
|
+
const pluginSkills = '/home/testuser/.agents/plugins/bigquery/skills';
|
|
799
|
+
stubSkillsRoot(pluginSkills, 'query-builder');
|
|
800
|
+
stubSkillsRoot('/home/testuser/.theia-ide/skills', 'code-review');
|
|
801
|
+
|
|
802
|
+
const failing: SkillDirectoryContribution = {
|
|
803
|
+
getSkillDirectories: () => { throw new Error('cannot enumerate plugins'); }
|
|
804
|
+
};
|
|
805
|
+
const service = createService([
|
|
806
|
+
failing,
|
|
807
|
+
contributing({ path: pluginSkills, tier: 'plugin', qualifier: bigQueryQualifier })
|
|
808
|
+
]);
|
|
809
|
+
await initialize(service);
|
|
810
|
+
|
|
811
|
+
expect(service.getSkills().map(skill => skill.qualifiedName).sort())
|
|
812
|
+
.to.deep.equal(['bigquery-data-analytics:query-builder', 'code-review']);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('watches the parent of a contributed root that does not exist yet instead of warning about it', async () => {
|
|
816
|
+
const pluginRoot = '/home/testuser/.agents/plugins/bigquery';
|
|
817
|
+
fileServiceMock.exists
|
|
818
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === `${pluginRoot}/skills`))
|
|
819
|
+
.resolves(false);
|
|
820
|
+
fileServiceMock.exists
|
|
821
|
+
.withArgs(sinon.match((uri: URI) => uri.path.toString() === pluginRoot))
|
|
822
|
+
.resolves(true);
|
|
823
|
+
|
|
824
|
+
const service = createService([contributing({ path: `${pluginRoot}/skills`, tier: 'plugin', qualifier: bigQueryQualifier })]);
|
|
825
|
+
await initialize(service);
|
|
826
|
+
|
|
827
|
+
expect(fileServiceMock.watch.calledWith(
|
|
828
|
+
sinon.match((uri: URI) => uri.path.toString() === pluginRoot),
|
|
829
|
+
sinon.match({ recursive: false, excludes: [] })
|
|
830
|
+
)).to.be.true;
|
|
831
|
+
// A plugin without skills is not a misconfiguration, so it must not warn like a
|
|
832
|
+
// non-existent configured directory does.
|
|
833
|
+
expect(loggerWarnSpy.calledWith(sinon.match(/does not exist/))).to.be.false;
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it('rescans when a contribution reports that its set of roots changed', async () => {
|
|
837
|
+
const pluginSkills = '/home/testuser/.agents/plugins/bigquery/skills';
|
|
838
|
+
const onDidChangeEmitter = new Emitter<void>();
|
|
839
|
+
let entries: SkillDirectoryEntry[] = [];
|
|
840
|
+
const contribution: SkillDirectoryContribution = {
|
|
841
|
+
getSkillDirectories: () => entries,
|
|
842
|
+
onDidChange: onDidChangeEmitter.event
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const service = createService([contribution]);
|
|
846
|
+
await initialize(service);
|
|
847
|
+
expect(service.getSkills()).to.have.length(0);
|
|
848
|
+
|
|
849
|
+
stubSkillsRoot(pluginSkills, 'query-builder');
|
|
850
|
+
entries = [{ path: pluginSkills, tier: 'plugin', qualifier: bigQueryQualifier }];
|
|
851
|
+
onDidChangeEmitter.fire();
|
|
852
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
853
|
+
|
|
854
|
+
expect(service.getSkills().map(skill => skill.qualifiedName)).to.deep.equal(['bigquery-data-analytics:query-builder']);
|
|
855
|
+
onDidChangeEmitter.dispose();
|
|
856
|
+
});
|
|
857
|
+
});
|
|
633
858
|
});
|
|
634
859
|
});
|