@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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
18
18
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
19
|
-
import { DisposableCollection, Emitter, Event, ILogger, URI } from '@theia/core';
|
|
19
|
+
import { ContributionProvider, Disposable, DisposableCollection, Emitter, Event, ILogger, MaybePromise, URI } from '@theia/core';
|
|
20
20
|
import { Path } from '@theia/core/lib/common/path';
|
|
21
21
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
22
22
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
@@ -26,6 +26,7 @@ import { AICorePreferences, PREFERENCE_NAME_SKILL_DIRECTORIES } from '../common/
|
|
|
26
26
|
import {
|
|
27
27
|
Skill,
|
|
28
28
|
SkillDescription,
|
|
29
|
+
SkillDirectoryEntry,
|
|
29
30
|
SKILL_FILE_NAME,
|
|
30
31
|
validateSkillDescription,
|
|
31
32
|
parseSkillFile,
|
|
@@ -35,12 +36,28 @@ import {
|
|
|
35
36
|
/** Debounce delay for coalescing rapid file system events */
|
|
36
37
|
const UPDATE_DEBOUNCE_MS = 50;
|
|
37
38
|
|
|
39
|
+
export const SkillDirectoryContribution = Symbol('SkillDirectoryContribution');
|
|
40
|
+
/**
|
|
41
|
+
* Contributes further roots to scan for skills. Point this at the *parent* of the skill folders,
|
|
42
|
+
* never at a skill folder itself. Contributed roots form the lowest-precedence `plugin` tier, so a
|
|
43
|
+
* directory the user controls always wins a path collision.
|
|
44
|
+
*/
|
|
45
|
+
export interface SkillDirectoryContribution {
|
|
46
|
+
/** Entries carrying a {@link SkillDirectoryEntry.qualifier} have their skills prefixed with it. */
|
|
47
|
+
getSkillDirectories(): MaybePromise<SkillDirectoryEntry[]>;
|
|
48
|
+
readonly onDidChange?: Event<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
export const SkillService = Symbol('SkillService');
|
|
39
52
|
export interface SkillService {
|
|
40
53
|
/** Get all discovered skills */
|
|
41
54
|
getSkills(): Skill[];
|
|
42
55
|
|
|
43
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Matched against {@link Skill.qualifiedName} first, then the plain
|
|
58
|
+
* {@link SkillDescription.name} so unqualified references keep resolving. A plain name shared by
|
|
59
|
+
* several skills is ambiguous and resolves to `undefined`.
|
|
60
|
+
*/
|
|
44
61
|
getSkill(name: string): Skill | undefined;
|
|
45
62
|
|
|
46
63
|
/** Event fired when skills change */
|
|
@@ -51,15 +68,14 @@ export interface SkillService {
|
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
@injectable()
|
|
54
|
-
export class DefaultSkillService implements SkillService {
|
|
71
|
+
export class DefaultSkillService implements SkillService, Disposable {
|
|
55
72
|
@inject(AICorePreferences)
|
|
56
73
|
protected readonly preferences: AICorePreferences;
|
|
57
74
|
|
|
58
75
|
@inject(FileService)
|
|
59
76
|
protected readonly fileService: FileService;
|
|
60
77
|
|
|
61
|
-
@inject(ILogger)
|
|
62
|
-
@named('SkillService')
|
|
78
|
+
@inject(ILogger) @named('ai-core:DefaultSkillService')
|
|
63
79
|
protected readonly logger: ILogger;
|
|
64
80
|
|
|
65
81
|
@inject(EnvVariablesServer)
|
|
@@ -68,8 +84,16 @@ export class DefaultSkillService implements SkillService {
|
|
|
68
84
|
@inject(WorkspaceService)
|
|
69
85
|
protected readonly workspaceService: WorkspaceService;
|
|
70
86
|
|
|
87
|
+
@inject(ContributionProvider)
|
|
88
|
+
@named(SkillDirectoryContribution)
|
|
89
|
+
protected readonly skillDirectoryContributions: ContributionProvider<SkillDirectoryContribution>;
|
|
90
|
+
|
|
91
|
+
/** Keyed by {@link Skill.qualifiedName}. */
|
|
71
92
|
protected skills = new Map<string, Skill>();
|
|
93
|
+
/** Watchers of the current scan; disposed and rebuilt on every rescan. */
|
|
72
94
|
protected toDispose = new DisposableCollection();
|
|
95
|
+
/** Kept apart from {@link toDispose}, which does not survive a rescan. */
|
|
96
|
+
protected readonly toDisposeOnServiceDispose = new DisposableCollection();
|
|
73
97
|
protected watchedDirectories = new Set<string>();
|
|
74
98
|
protected parentWatchers = new Map<string, string>();
|
|
75
99
|
|
|
@@ -91,6 +115,12 @@ export class DefaultSkillService implements SkillService {
|
|
|
91
115
|
|
|
92
116
|
@postConstruct()
|
|
93
117
|
protected init(): void {
|
|
118
|
+
for (const contribution of this.skillDirectoryContributions.getContributions()) {
|
|
119
|
+
if (contribution.onDidChange) {
|
|
120
|
+
this.toDisposeOnServiceDispose.push(contribution.onDidChange(() => this.scheduleUpdate()));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
this.fileService.onDidFilesChange(async (event: FileChangesEvent) => {
|
|
95
125
|
for (const change of event.changes) {
|
|
96
126
|
if (change.type === FileChangeType.ADDED) {
|
|
@@ -131,29 +161,43 @@ export class DefaultSkillService implements SkillService {
|
|
|
131
161
|
}
|
|
132
162
|
});
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.update().then(() => {
|
|
137
|
-
this._ready.resolve();
|
|
138
|
-
// Only after initial update, start listening for changes
|
|
139
|
-
this.lastSkillDirectoriesValue = JSON.stringify(this.preferences[PREFERENCE_NAME_SKILL_DIRECTORIES]);
|
|
140
|
-
|
|
141
|
-
this.preferences.onPreferenceChanged(event => {
|
|
142
|
-
if (event.preferenceName === PREFERENCE_NAME_SKILL_DIRECTORIES) {
|
|
143
|
-
const currentValue = JSON.stringify(this.preferences[PREFERENCE_NAME_SKILL_DIRECTORIES]);
|
|
144
|
-
if (currentValue === this.lastSkillDirectoriesValue) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
this.lastSkillDirectoriesValue = currentValue;
|
|
148
|
-
this.scheduleUpdate();
|
|
149
|
-
}
|
|
150
|
-
});
|
|
164
|
+
this.initializeSkills();
|
|
165
|
+
}
|
|
151
166
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
/** Runs the initial scan once the skill directory sources are known. {@link ready} always resolves, even if the scan failed. */
|
|
168
|
+
protected async initializeSkills(): Promise<void> {
|
|
169
|
+
try {
|
|
170
|
+
// The workspace contributes the workspace skill directories, the preferences the configured ones.
|
|
171
|
+
await Promise.all([this.workspaceService.ready, this.preferences.ready]);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.logger.error('Failed to resolve skill directory sources, scanning the already known directories', error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Listen for changes before the initial scan, otherwise a change landing while it runs is recorded as
|
|
177
|
+
// already applied by the snapshot below and dropped. update()'s in-progress guard coalesces the overlap.
|
|
178
|
+
this.lastSkillDirectoriesValue = JSON.stringify(this.preferences[PREFERENCE_NAME_SKILL_DIRECTORIES]);
|
|
179
|
+
|
|
180
|
+
this.preferences.onPreferenceChanged(event => {
|
|
181
|
+
if (event.preferenceName === PREFERENCE_NAME_SKILL_DIRECTORIES) {
|
|
182
|
+
const currentValue = JSON.stringify(this.preferences[PREFERENCE_NAME_SKILL_DIRECTORIES]);
|
|
183
|
+
if (currentValue === this.lastSkillDirectoriesValue) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.lastSkillDirectoriesValue = currentValue;
|
|
187
|
+
this.scheduleUpdate();
|
|
188
|
+
}
|
|
156
189
|
});
|
|
190
|
+
|
|
191
|
+
this.workspaceService.onWorkspaceChanged(() => {
|
|
192
|
+
this.scheduleUpdate();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await this.update();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.logger.error('Initial skill scan failed', error);
|
|
199
|
+
}
|
|
200
|
+
this._ready.resolve();
|
|
157
201
|
}
|
|
158
202
|
|
|
159
203
|
getSkills(): Skill[] {
|
|
@@ -161,7 +205,19 @@ export class DefaultSkillService implements SkillService {
|
|
|
161
205
|
}
|
|
162
206
|
|
|
163
207
|
getSkill(name: string): Skill | undefined {
|
|
164
|
-
|
|
208
|
+
const qualifiedMatch = this.skills.get(name);
|
|
209
|
+
if (qualifiedMatch) {
|
|
210
|
+
return qualifiedMatch;
|
|
211
|
+
}
|
|
212
|
+
// Ambiguous plain names resolve to nothing: guessing would silently load a skill the caller
|
|
213
|
+
// did not ask for.
|
|
214
|
+
const plainMatches = this.getSkills().filter(skill => skill.name === name);
|
|
215
|
+
return plainMatches.length === 1 ? plainMatches[0] : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
dispose(): void {
|
|
219
|
+
this.toDisposeOnServiceDispose.dispose();
|
|
220
|
+
this.toDispose.dispose();
|
|
165
221
|
}
|
|
166
222
|
|
|
167
223
|
protected scheduleUpdate(): void {
|
|
@@ -213,21 +269,25 @@ export class DefaultSkillService implements SkillService {
|
|
|
213
269
|
const configuredDirectories = (this.preferences[PREFERENCE_NAME_SKILL_DIRECTORIES] ?? [])
|
|
214
270
|
.map(dir => Path.untildify(dir, homePath));
|
|
215
271
|
const defaultSkillsDirs = await this.getDefaultSkillsDirectoryPaths();
|
|
272
|
+
const pluginSkillsDirs = await this.getContributedSkillDirectories();
|
|
216
273
|
|
|
217
274
|
const newWatchedDirectories = new Set<string>();
|
|
218
275
|
const newParentWatchers = new Map<string, string>();
|
|
219
276
|
|
|
220
|
-
const allDirectories = combineSkillDirectories(workspaceSkillsDirs, configuredDirectories, defaultSkillsDirs);
|
|
221
|
-
for (const { path: directoryPath, tier } of allDirectories) {
|
|
277
|
+
const allDirectories = combineSkillDirectories(workspaceSkillsDirs, configuredDirectories, defaultSkillsDirs, pluginSkillsDirs);
|
|
278
|
+
for (const { path: directoryPath, tier, qualifier } of allDirectories) {
|
|
222
279
|
if (tier === 'configured') {
|
|
223
|
-
await this.processConfiguredSkillDirectory(directoryPath, newSkills, newDisposables, newWatchedDirectories);
|
|
280
|
+
await this.processConfiguredSkillDirectory(directoryPath, newSkills, newDisposables, newWatchedDirectories, qualifier);
|
|
224
281
|
} else {
|
|
282
|
+
// `plugin` roots take this path too: a plugin with no `skills` directory is a
|
|
283
|
+
// plugin without skills, worth watching for rather than warning about.
|
|
225
284
|
await this.processSkillDirectoryWithParentWatching(
|
|
226
285
|
directoryPath,
|
|
227
286
|
newSkills,
|
|
228
287
|
newDisposables,
|
|
229
288
|
newWatchedDirectories,
|
|
230
|
-
newParentWatchers
|
|
289
|
+
newParentWatchers,
|
|
290
|
+
qualifier
|
|
231
291
|
);
|
|
232
292
|
}
|
|
233
293
|
}
|
|
@@ -244,6 +304,19 @@ export class DefaultSkillService implements SkillService {
|
|
|
244
304
|
this.onSkillsChangedEmitter.fire();
|
|
245
305
|
}
|
|
246
306
|
|
|
307
|
+
/** A failing contribution is skipped: one broken root must not cost every other skill. */
|
|
308
|
+
protected async getContributedSkillDirectories(): Promise<SkillDirectoryEntry[]> {
|
|
309
|
+
const entries: SkillDirectoryEntry[] = [];
|
|
310
|
+
for (const contribution of this.skillDirectoryContributions.getContributions()) {
|
|
311
|
+
try {
|
|
312
|
+
entries.push(...await contribution.getSkillDirectories());
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.logger.error(`Failed to collect contributed skill directories: ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return entries;
|
|
318
|
+
}
|
|
319
|
+
|
|
247
320
|
protected getWorkspaceSkillsDirectoryPaths(): string[] {
|
|
248
321
|
return this.workspaceService.tryGetRoots().flatMap(root => [
|
|
249
322
|
root.resource.resolve('.prompts/skills').path.fsPath(),
|
|
@@ -267,7 +340,8 @@ export class DefaultSkillService implements SkillService {
|
|
|
267
340
|
skills: Map<string, Skill>,
|
|
268
341
|
disposables: DisposableCollection,
|
|
269
342
|
watchedDirectories: Set<string>,
|
|
270
|
-
parentWatchers: Map<string, string
|
|
343
|
+
parentWatchers: Map<string, string>,
|
|
344
|
+
qualifier?: string
|
|
271
345
|
): Promise<void> {
|
|
272
346
|
const dirURI = URI.fromFilePath(directoryPath);
|
|
273
347
|
|
|
@@ -275,7 +349,7 @@ export class DefaultSkillService implements SkillService {
|
|
|
275
349
|
const dirExists = await this.fileService.exists(dirURI);
|
|
276
350
|
|
|
277
351
|
if (dirExists) {
|
|
278
|
-
await this.processExistingSkillDirectory(dirURI, skills, disposables, watchedDirectories);
|
|
352
|
+
await this.processExistingSkillDirectory(dirURI, skills, disposables, watchedDirectories, qualifier);
|
|
279
353
|
} else {
|
|
280
354
|
const parentPath = dirURI.parent.path.fsPath();
|
|
281
355
|
const parentURI = URI.fromFilePath(parentPath);
|
|
@@ -299,7 +373,8 @@ export class DefaultSkillService implements SkillService {
|
|
|
299
373
|
directoryPath: string,
|
|
300
374
|
skills: Map<string, Skill>,
|
|
301
375
|
disposables: DisposableCollection,
|
|
302
|
-
watchedDirectories: Set<string
|
|
376
|
+
watchedDirectories: Set<string>,
|
|
377
|
+
qualifier?: string
|
|
303
378
|
): Promise<void> {
|
|
304
379
|
const dirURI = URI.fromFilePath(directoryPath);
|
|
305
380
|
|
|
@@ -311,7 +386,7 @@ export class DefaultSkillService implements SkillService {
|
|
|
311
386
|
return;
|
|
312
387
|
}
|
|
313
388
|
|
|
314
|
-
await this.processExistingSkillDirectory(dirURI, skills, disposables, watchedDirectories);
|
|
389
|
+
await this.processExistingSkillDirectory(dirURI, skills, disposables, watchedDirectories, qualifier);
|
|
315
390
|
} catch (error) {
|
|
316
391
|
this.logger.error(`Error processing configured directory '${directoryPath}': ${error}`);
|
|
317
392
|
}
|
|
@@ -321,7 +396,8 @@ export class DefaultSkillService implements SkillService {
|
|
|
321
396
|
dirURI: URI,
|
|
322
397
|
skills: Map<string, Skill>,
|
|
323
398
|
disposables: DisposableCollection,
|
|
324
|
-
watchedDirectories: Set<string
|
|
399
|
+
watchedDirectories: Set<string>,
|
|
400
|
+
qualifier?: string
|
|
325
401
|
): Promise<void> {
|
|
326
402
|
const stat = await this.fileService.resolve(dirURI);
|
|
327
403
|
if (!stat.children) {
|
|
@@ -331,14 +407,14 @@ export class DefaultSkillService implements SkillService {
|
|
|
331
407
|
for (const child of stat.children) {
|
|
332
408
|
if (child.isDirectory) {
|
|
333
409
|
const directoryName = child.name;
|
|
334
|
-
await this.loadSkillFromDirectory(child.resource, directoryName, skills);
|
|
410
|
+
await this.loadSkillFromDirectory(child.resource, directoryName, skills, qualifier);
|
|
335
411
|
}
|
|
336
412
|
}
|
|
337
413
|
|
|
338
414
|
this.setupDirectoryWatcher(dirURI, disposables, watchedDirectories);
|
|
339
415
|
}
|
|
340
416
|
|
|
341
|
-
protected async loadSkillFromDirectory(directoryUri: URI, directoryName: string, skills: Map<string, Skill
|
|
417
|
+
protected async loadSkillFromDirectory(directoryUri: URI, directoryName: string, skills: Map<string, Skill>, qualifier?: string): Promise<void> {
|
|
342
418
|
const skillFileUri = directoryUri.resolve(SKILL_FILE_NAME);
|
|
343
419
|
|
|
344
420
|
const fileExists = await this.fileService.exists(skillFileUri);
|
|
@@ -366,19 +442,20 @@ export class DefaultSkillService implements SkillService {
|
|
|
366
442
|
return;
|
|
367
443
|
}
|
|
368
444
|
|
|
369
|
-
const
|
|
445
|
+
const qualifiedName = Skill.qualifyName(parsed.metadata.name, qualifier);
|
|
370
446
|
|
|
371
|
-
if (skills.has(
|
|
372
|
-
this.logger.warn(`Skill '${
|
|
447
|
+
if (skills.has(qualifiedName)) {
|
|
448
|
+
this.logger.warn(`Skill '${qualifiedName}': Duplicate skill found in '${directoryName}', using first discovered instance`);
|
|
373
449
|
return;
|
|
374
450
|
}
|
|
375
451
|
|
|
376
452
|
const skill: Skill = {
|
|
377
453
|
...parsed.metadata,
|
|
378
|
-
location: skillFileUri.path.fsPath()
|
|
454
|
+
location: skillFileUri.path.fsPath(),
|
|
455
|
+
qualifiedName
|
|
379
456
|
};
|
|
380
457
|
|
|
381
|
-
skills.set(
|
|
458
|
+
skills.set(qualifiedName, skill);
|
|
382
459
|
} catch (error) {
|
|
383
460
|
this.logger.error(`Failed to load skill from '${directoryName}': ${error}`);
|
|
384
461
|
}
|
|
@@ -163,11 +163,13 @@ describe('SkillsVariableContribution', () => {
|
|
|
163
163
|
const skills: Skill[] = [
|
|
164
164
|
{
|
|
165
165
|
name: 'pdf-processing',
|
|
166
|
+
qualifiedName: 'pdf-processing',
|
|
166
167
|
description: 'Processes PDF documents and extracts text content',
|
|
167
168
|
location: '/path/to/skills/pdf-processing/SKILL.md'
|
|
168
169
|
},
|
|
169
170
|
{
|
|
170
171
|
name: 'data-analysis',
|
|
172
|
+
qualifiedName: 'data-analysis',
|
|
171
173
|
description: 'Analyzes data sets and generates reports',
|
|
172
174
|
location: '/path/to/skills/data-analysis/SKILL.md'
|
|
173
175
|
}
|
|
@@ -207,6 +209,7 @@ describe('SkillsVariableContribution', () => {
|
|
|
207
209
|
const skills: Skill[] = [
|
|
208
210
|
{
|
|
209
211
|
name: 'test-skill',
|
|
212
|
+
qualifiedName: 'test-skill',
|
|
210
213
|
description: 'Handles <tags> & "quotes" with \'apostrophes\'',
|
|
211
214
|
location: '/path/to/skill/SKILL.md'
|
|
212
215
|
}
|
|
@@ -228,6 +231,7 @@ describe('SkillsVariableContribution', () => {
|
|
|
228
231
|
const skills: Skill[] = [
|
|
229
232
|
{
|
|
230
233
|
name: 'skill<test>',
|
|
234
|
+
qualifiedName: 'skill<test>',
|
|
231
235
|
description: 'Test skill',
|
|
232
236
|
location: '/path/with/&special/chars'
|
|
233
237
|
}
|
|
@@ -266,6 +270,7 @@ describe('SkillsVariableContribution', () => {
|
|
|
266
270
|
it('should return skill content when skill found', async () => {
|
|
267
271
|
const skill: Skill = {
|
|
268
272
|
name: 'my-skill',
|
|
273
|
+
qualifiedName: 'my-skill',
|
|
269
274
|
description: 'A test skill',
|
|
270
275
|
location: '/path/to/skills/my-skill/SKILL.md'
|
|
271
276
|
};
|
|
@@ -293,6 +298,7 @@ This is the skill content.`
|
|
|
293
298
|
it('should return undefined when file read fails', async () => {
|
|
294
299
|
const skill: Skill = {
|
|
295
300
|
name: 'my-skill',
|
|
301
|
+
qualifiedName: 'my-skill',
|
|
296
302
|
description: 'A test skill',
|
|
297
303
|
location: '/path/to/skills/my-skill/SKILL.md'
|
|
298
304
|
};
|
|
@@ -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
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
17
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
18
18
|
import { ILogger, MaybePromise, nls, URI } from '@theia/core';
|
|
19
19
|
import {
|
|
20
20
|
AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from '../common/variable-service';
|
|
23
23
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
24
24
|
import { SkillService } from './skill-service';
|
|
25
|
-
import { parseSkillFile } from '../common/skill';
|
|
25
|
+
import { parseSkillFile, Skill } from '../common/skill';
|
|
26
26
|
|
|
27
27
|
export const SKILLS_VARIABLE: AIVariable = {
|
|
28
28
|
id: 'skills',
|
|
@@ -39,6 +39,7 @@ export const SKILL_VARIABLE: AIVariable = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export interface SkillSummary {
|
|
42
|
+
/** The name the model has to write to address the skill, i.e. {@link Skill.qualifiedName}. */
|
|
42
43
|
name: string;
|
|
43
44
|
description: string;
|
|
44
45
|
location: string;
|
|
@@ -54,7 +55,7 @@ export class SkillsVariableContribution implements AIVariableContribution, AIVar
|
|
|
54
55
|
@inject(SkillService)
|
|
55
56
|
protected readonly skillService: SkillService;
|
|
56
57
|
|
|
57
|
-
@inject(ILogger)
|
|
58
|
+
@inject(ILogger) @named('ai-core:SkillsVariableContribution')
|
|
58
59
|
protected readonly logger: ILogger;
|
|
59
60
|
|
|
60
61
|
@inject(FileService)
|
|
@@ -80,11 +81,20 @@ export class SkillsVariableContribution implements AIVariableContribution, AIVar
|
|
|
80
81
|
|
|
81
82
|
// Handle plural skills variable
|
|
82
83
|
if (request.variable.name === SKILLS_VARIABLE.name) {
|
|
83
|
-
return this.resolveSkillsVariable(this.skillService.getSkills(), SKILLS_VARIABLE);
|
|
84
|
+
return this.resolveSkillsVariable(this.skillService.getSkills().map(skill => this.toSkillSummary(skill)), SKILLS_VARIABLE);
|
|
84
85
|
}
|
|
85
86
|
return undefined;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/** Named by {@link Skill.qualifiedName}: that is what the model has to write to load it again. */
|
|
90
|
+
toSkillSummary(skill: Skill): SkillSummary {
|
|
91
|
+
return {
|
|
92
|
+
name: skill.qualifiedName,
|
|
93
|
+
description: skill.description,
|
|
94
|
+
location: skill.location
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
/**
|
|
89
99
|
* Resolves skills into a ResolvedSkillsVariable with XML format.
|
|
90
100
|
*/
|
|
@@ -51,40 +51,40 @@ export class TheiaVariableContribution implements AIVariableContribution, AIVari
|
|
|
51
51
|
['file', [
|
|
52
52
|
{
|
|
53
53
|
name: 'currentAbsoluteFilePath',
|
|
54
|
-
description: nls.localize('theia/ai/core/variable-contribution/currentAbsoluteFilePath',
|
|
55
|
-
|
|
54
|
+
description: nls.localize('theia/ai/core/variable-contribution/currentAbsoluteFilePath',
|
|
55
|
+
'The absolute path of the currently opened file.')
|
|
56
56
|
}
|
|
57
57
|
]],
|
|
58
58
|
['selectedText', [
|
|
59
59
|
{
|
|
60
|
-
description: nls.localize('theia/ai/core/variable-contribution/currentSelectedText',
|
|
61
|
-
|
|
60
|
+
description: nls.localize('theia/ai/core/variable-contribution/currentSelectedText',
|
|
61
|
+
'The plain text that is currently selected in the opened file. This excludes the information where the content is coming from.')
|
|
62
62
|
}
|
|
63
63
|
]],
|
|
64
64
|
['currentText', [
|
|
65
65
|
{
|
|
66
66
|
name: 'currentFileContent',
|
|
67
|
-
description: nls.localize('theia/ai/core/variable-contribution/currentFileContent',
|
|
68
|
-
|
|
67
|
+
description: nls.localize('theia/ai/core/variable-contribution/currentFileContent',
|
|
68
|
+
'The plain content of the currently opened file. This excludes the information where the content is coming from.')
|
|
69
69
|
}
|
|
70
70
|
]],
|
|
71
71
|
['relativeFile', [
|
|
72
72
|
{
|
|
73
73
|
name: 'currentRelativeFilePath',
|
|
74
|
-
description: nls.localize('theia/ai/core/variable-contribution/currentRelativeFilePath',
|
|
75
|
-
|
|
74
|
+
description: nls.localize('theia/ai/core/variable-contribution/currentRelativeFilePath',
|
|
75
|
+
'The workspace-relative path of the currently opened file (e.g., my-project/src/index.ts).')
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
name: '_f',
|
|
79
|
-
description: nls.localize('theia/ai/core/variable-contribution/dotRelativePath',
|
|
80
|
-
|
|
79
|
+
description: nls.localize('theia/ai/core/variable-contribution/dotRelativePath',
|
|
80
|
+
'Short reference to the workspace-relative path of the currently opened file (\'currentRelativeFilePath\').')
|
|
81
81
|
}
|
|
82
82
|
]],
|
|
83
83
|
['relativeFileDirname', [
|
|
84
84
|
{
|
|
85
85
|
name: 'currentRelativeDirPath',
|
|
86
|
-
description: nls.localize('theia/ai/core/variable-contribution/currentRelativeDirPath',
|
|
87
|
-
|
|
86
|
+
description: nls.localize('theia/ai/core/variable-contribution/currentRelativeDirPath',
|
|
87
|
+
'The workspace-relative path of the directory containing the currently opened file (e.g., my-project/src).')
|
|
88
88
|
}
|
|
89
89
|
]],
|
|
90
90
|
['lineNumber', [{}]],
|
|
@@ -28,6 +28,25 @@ import { ReasoningSettings, SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD_MINIMUM } fro
|
|
|
28
28
|
import { PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
|
|
29
29
|
|
|
30
30
|
export const AI_CORE_PREFERENCES_TITLE = nls.localize('theia/ai-core/preferences/title', 'AI Features');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Key under a preference's schema `typeDetails` where a language-model provider declares the
|
|
34
|
+
* human-readable name of its provider block. The AI Configuration view's Models page reads this to
|
|
35
|
+
* label the provider node (discovered from the `ai-features.<provider>.*` preferences) without
|
|
36
|
+
* having to hard-code provider names. Declare it on any one preference of the provider's block, e.g.
|
|
37
|
+
* `typeDetails: { [MODEL_PROVIDER_TYPE_DETAIL]: 'Anthropic' }`.
|
|
38
|
+
*/
|
|
39
|
+
export const MODEL_PROVIDER_TYPE_DETAIL = 'aiModelProvider';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Shape a language-model provider stores under a preference's `typeDetails[MODEL_PROVIDER_TYPE_DETAIL]`
|
|
43
|
+
* to describe its `ai-features.<provider>.*` block to the AI Configuration view's Models page. Declared
|
|
44
|
+
* on any one preference of the block, e.g. `typeDetails: { [MODEL_PROVIDER_TYPE_DETAIL]: { label: 'Anthropic' } }`.
|
|
45
|
+
*/
|
|
46
|
+
export interface ModelProviderTypeDetail {
|
|
47
|
+
/** Human-readable provider name shown as the provider node/card label. */
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
31
50
|
export const PREFERENCE_NAME_PROMPT_TEMPLATES = 'ai-features.promptTemplates.promptTemplatesFolder';
|
|
32
51
|
export const PREFERENCE_NAME_REQUEST_SETTINGS = 'ai-features.modelSettings.requestSettings';
|
|
33
52
|
export const PREFERENCE_NAME_REASONING = 'ai-features.reasoning.defaults';
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import * as deepEqual from 'fast-deep-equal';
|
|
18
|
-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
19
|
-
import { Resource, URI, generateUuid } from '@theia/core';
|
|
18
|
+
import { inject, injectable, postConstruct, named } from '@theia/core/shared/inversify';
|
|
19
|
+
import { Resource, URI, generateUuid, ILogger } from '@theia/core';
|
|
20
20
|
import { AIVariableContext, AIVariableResolutionRequest } from './variable-service';
|
|
21
21
|
import stableJsonStringify = require('fast-json-stable-stringify');
|
|
22
22
|
import { ConfigurableInMemoryResources, ConfigurableMutableReferenceResource } from './configurable-in-memory-resources';
|
|
@@ -28,6 +28,9 @@ export const NO_CONTEXT_AUTHORITY = 'context-free';
|
|
|
28
28
|
export class AIVariableResourceResolver {
|
|
29
29
|
@inject(ConfigurableInMemoryResources) protected readonly inMemoryResources: ConfigurableInMemoryResources;
|
|
30
30
|
|
|
31
|
+
@inject(ILogger) @named('ai-core:AIVariableResourceResolver')
|
|
32
|
+
protected readonly logger: ILogger;
|
|
33
|
+
|
|
31
34
|
@postConstruct()
|
|
32
35
|
protected init(): void {
|
|
33
36
|
this.inMemoryResources.onWillDispose(resource => this.cache.delete(resource.uri.toString()));
|
|
@@ -68,7 +71,7 @@ export class AIVariableResourceResolver {
|
|
|
68
71
|
}
|
|
69
72
|
} catch (err) {
|
|
70
73
|
// Mostly that deep equal could overflow the stack, but it should run into === or inequality before that.
|
|
71
|
-
|
|
74
|
+
this.logger.warn('Problem evaluating context in AIVariableResourceResolver', err);
|
|
72
75
|
}
|
|
73
76
|
return generateUuid();
|
|
74
77
|
}
|
|
@@ -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
|
import { ILogger, nls } from '@theia/core';
|
|
17
|
-
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
17
|
+
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
|
18
18
|
import * as monaco from '@theia/monaco-editor-core';
|
|
19
19
|
import {
|
|
20
20
|
AIVariable,
|
|
@@ -49,8 +49,8 @@ export class CapabilityVariableContribution implements AIVariableContribution, A
|
|
|
49
49
|
@inject(PromptService)
|
|
50
50
|
protected readonly promptService: PromptService;
|
|
51
51
|
|
|
52
|
-
@inject(ILogger)
|
|
53
|
-
protected logger: ILogger;
|
|
52
|
+
@inject(ILogger) @named('ai-core:CapabilityVariableContribution')
|
|
53
|
+
protected readonly logger: ILogger;
|
|
54
54
|
|
|
55
55
|
registerVariables(service: AIVariableService): void {
|
|
56
56
|
service.registerResolver(CAPABILITY_VARIABLE, this);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
isLanguageModelStreamResponsePart,
|
|
22
22
|
isModelMatching,
|
|
23
23
|
isToolCallContent,
|
|
24
|
+
isToolCallHtmlAppResult,
|
|
24
25
|
LanguageModel,
|
|
25
26
|
LanguageModelSelector,
|
|
26
27
|
resolveCompactionDefault,
|
|
@@ -260,3 +261,85 @@ describe('compaction contract', () => {
|
|
|
260
261
|
expect(resolveServerSideCompaction(true, true, { enabled: false })).to.equal(false);
|
|
261
262
|
});
|
|
262
263
|
});
|
|
264
|
+
|
|
265
|
+
describe('isToolCallHtmlAppResult', () => {
|
|
266
|
+
it('returns true for valid html app result', () => {
|
|
267
|
+
expect(isToolCallHtmlAppResult({ type: 'html', html: '<div>Hello</div>' })).to.be.true;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('returns true with optional title', () => {
|
|
271
|
+
expect(isToolCallHtmlAppResult({ type: 'html', html: '<p>App</p>', title: 'My App' })).to.be.true;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns false for text result', () => {
|
|
275
|
+
expect(isToolCallHtmlAppResult({ type: 'text', text: 'hello' })).to.be.false;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns false for missing html field', () => {
|
|
279
|
+
expect(isToolCallHtmlAppResult({ type: 'html' })).to.be.false;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('returns false for null', () => {
|
|
283
|
+
expect(isToolCallHtmlAppResult(undefined)).to.be.false;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns false for undefined', () => {
|
|
287
|
+
expect(isToolCallHtmlAppResult(undefined)).to.be.false;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('returns true for full iframe-ready html app result', () => {
|
|
291
|
+
const html = `<!DOCTYPE html>
|
|
292
|
+
<html lang="en">
|
|
293
|
+
<head>
|
|
294
|
+
<meta charset="UTF-8" />
|
|
295
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
296
|
+
<title>Animated Unit Test Sandbox</title>
|
|
297
|
+
<style>
|
|
298
|
+
html, body { margin: 0; height: 100%; overflow: hidden; background: #0f172a; font-family: Inter, Arial, sans-serif; }
|
|
299
|
+
.scene { position: relative; width: 100%; height: 100%; background: radial-gradient(circle at top, #1e293b 0%, #020617 70%); overflow: hidden; }
|
|
300
|
+
.grid { position: absolute; inset: 0; background-size: 40px 40px; animation: drift 12s linear infinite;
|
|
301
|
+
background-image: linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px),
|
|
302
|
+
linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px); }
|
|
303
|
+
@keyframes drift { from { transform: translateY(0px); } to { transform: translateY(40px); } }
|
|
304
|
+
.panel { position: absolute; top: 50%; left: 50%; width: 340px;
|
|
305
|
+
transform: translate(-50%, -50%); background: rgba(15, 23, 42, 0.85);
|
|
306
|
+
border: 1px solid rgba(255,255,255,0.1); border-radius: 20px; padding: 24px;
|
|
307
|
+
box-shadow: 0 0 50px rgba(59,130,246,0.3), inset 0 0 30px rgba(255,255,255,0.03);
|
|
308
|
+
backdrop-filter: blur(10px); }
|
|
309
|
+
.title { color: white; font-size: 24px; font-weight: 700; margin-bottom: 8px; }
|
|
310
|
+
.subtitle { color: #94a3b8; margin-bottom: 24px; }
|
|
311
|
+
.test { display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
|
|
312
|
+
color: white; font-size: 14px; opacity: 0; transform: translateX(-10px);
|
|
313
|
+
animation: reveal 0.5s forwards; }
|
|
314
|
+
.test:nth-child(3) { animation-delay: 0.2s; }
|
|
315
|
+
.test:nth-child(4) { animation-delay: 0.5s; }
|
|
316
|
+
.test:nth-child(5) { animation-delay: 0.8s; }
|
|
317
|
+
.test:nth-child(6) { animation-delay: 1.1s; }
|
|
318
|
+
@keyframes reveal { to { opacity: 1; transform: translateX(0); } }
|
|
319
|
+
.dot { width: 12px; height: 12px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 12px #22c55e; animation: pulse 1.5s infinite; }
|
|
320
|
+
.warn { background: #f59e0b; box-shadow: 0 0 12px #f59e0b; }
|
|
321
|
+
.fail { background: #ef4444; box-shadow: 0 0 12px #ef4444; }
|
|
322
|
+
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.4); opacity: 0.7; } }
|
|
323
|
+
.particle { position: absolute; border-radius: 50%; background: rgba(96,165,250,0.25); animation: float linear infinite; }
|
|
324
|
+
@keyframes float { from { transform: translateY(100vh) scale(0.5); } to { transform: translateY(-120px) scale(1.2); } }
|
|
325
|
+
.footer { margin-top: 20px; color: #64748b; font-size: 12px; text-align: center; }
|
|
326
|
+
</style>
|
|
327
|
+
</head>
|
|
328
|
+
<body>
|
|
329
|
+
<div class="scene">
|
|
330
|
+
<div class="grid"></div>
|
|
331
|
+
<div class="panel">
|
|
332
|
+
<div class="title">Unit Test Runner</div>
|
|
333
|
+
<div class="subtitle">Simulated CI Environment</div>
|
|
334
|
+
<div class="test"><div class="dot"></div>auth.service.spec.ts</div>
|
|
335
|
+
<div class="test"><div class="dot"></div>payments.integration.spec.ts</div>
|
|
336
|
+
<div class="test"><div class="dot warn"></div>websocket.reconnect.spec.ts</div>
|
|
337
|
+
<div class="test"><div class="dot fail"></div>cache.invalidation.spec.ts</div>
|
|
338
|
+
<div class="footer">24 passed • 1 flaky • 1 failed</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</body>
|
|
342
|
+
</html>`;
|
|
343
|
+
expect(isToolCallHtmlAppResult({ type: 'html', html, title: 'Animated Unit Test Sandbox' })).to.be.true;
|
|
344
|
+
});
|
|
345
|
+
});
|