Skip to content

Commit 359c905

Browse files
committed
Merge branch 'master' into 'opensource'
sync v1.0.303 See merge request ai_native/DeepVCode/DeepVcodeClient!333
2 parents 6c51fb2 + 895676f commit 359c905

15 files changed

Lines changed: 713 additions & 46 deletions

packages/core/src/skills/loaders/marketplace-loader.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,14 @@ export class MarketplaceLoader implements IPluginLoader {
266266
}
267267
}
268268

269-
// 3. 如果没有显式定义组件,且 strict !== false,则自动发现
270-
const isStrictMode = pluginDef.strict !== false;
271-
272-
if (components.length === 0 || !isStrictMode) {
269+
// 3. 自动发现组件
270+
// strict 字段语义:
271+
// - undefined (默认): 总是自动发现并合并,确保发现所有组件
272+
// - false: 总是自动发现并合并(显式声明)
273+
// - true: 只使用显式定义的组件,不自动发现
274+
const shouldAutoDiscover = pluginDef.strict !== true;
275+
276+
if (shouldAutoDiscover) {
273277
// 自动发现标准目录
274278
// 按照标准目录和常见第三方工具目录进行自动发现
275279
const discoveryTasks = [
@@ -283,11 +287,32 @@ export class MarketplaceLoader implements IPluginLoader {
283287
{ name: '.roo/commands', type: ComponentType.COMMAND },
284288
];
285289

290+
const discoveredComponents: UnifiedComponent[] = [];
286291
for (const task of discoveryTasks) {
287-
components.push(...await this.scanComponents(
292+
discoveredComponents.push(...await this.scanComponents(
288293
pluginDir, task.name, task.type, id, marketplaceId
289294
));
290295
}
296+
297+
// 合并显式定义的组件和自动发现的组件(去重)
298+
// 显式定义的组件优先(保持在前面),然后添加新发现的组件
299+
const existingIds = new Set(components.map(c => c.id));
300+
let addedCount = 0;
301+
for (const discovered of discoveredComponents) {
302+
if (!existingIds.has(discovered.id)) {
303+
components.push(discovered);
304+
existingIds.add(discovered.id);
305+
addedCount++;
306+
}
307+
}
308+
309+
// Debug logging for skill discovery
310+
if (process.env.DEBUG_SKILLS) {
311+
console.log(`[MarketplaceLoader] Plugin ${id}:`);
312+
console.log(` - Explicit components: ${components.length - addedCount}`);
313+
console.log(` - Auto-discovered: ${addedCount}`);
314+
console.log(` - Total: ${components.length}`);
315+
}
291316
}
292317

293318
// 4. 构建 UnifiedPlugin

packages/core/src/tools/use-skill.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,146 @@ describe('UseSkillTool', () => {
251251
expect(result).toBe(false);
252252
});
253253
});
254+
255+
describe('skill name matching', () => {
256+
it('should match skill by exact name (case-insensitive)', async () => {
257+
const mockSkill = {
258+
id: 'marketplace:plugin:pptx',
259+
name: 'pptx',
260+
description: 'Test skill',
261+
pluginId: 'marketplace:plugin',
262+
marketplaceId: 'marketplace',
263+
path: '/mock/path',
264+
skillFilePath: '/mock/path/SKILL.md',
265+
metadata: { name: 'pptx' },
266+
enabled: true,
267+
loadLevel: SkillLoadLevel.RESOURCES,
268+
scripts: [],
269+
};
270+
271+
mockLoaderInstance = {
272+
loadEnabledSkills: () => [mockSkill],
273+
};
274+
mockInjectorInstance = {
275+
loadSkillLevel2: () => '# Test\n\nTest content',
276+
};
277+
278+
// Test case-insensitive matching
279+
const result1 = await useSkillTool.execute({ skillName: 'pptx' }, mockAbortSignal);
280+
expect(result1.returnDisplay).toContain('✅ Loaded skill');
281+
282+
const result2 = await useSkillTool.execute({ skillName: 'PPTX' }, mockAbortSignal);
283+
expect(result2.returnDisplay).toContain('✅ Loaded skill');
284+
285+
const result3 = await useSkillTool.execute({ skillName: 'PpTx' }, mockAbortSignal);
286+
expect(result3.returnDisplay).toContain('✅ Loaded skill');
287+
});
288+
289+
it('should match skill by ID suffix', async () => {
290+
const mockSkill = {
291+
id: 'marketplace:plugin:my-skill',
292+
name: 'my-skill',
293+
description: 'Test skill',
294+
pluginId: 'marketplace:plugin',
295+
marketplaceId: 'marketplace',
296+
path: '/mock/path',
297+
skillFilePath: '/mock/path/SKILL.md',
298+
metadata: { name: 'my-skill' },
299+
enabled: true,
300+
loadLevel: SkillLoadLevel.RESOURCES,
301+
scripts: [],
302+
};
303+
304+
mockLoaderInstance = {
305+
loadEnabledSkills: () => [mockSkill],
306+
};
307+
mockInjectorInstance = {
308+
loadSkillLevel2: () => '# Test\n\nTest content',
309+
};
310+
311+
// Should match by just the skill name
312+
const result = await useSkillTool.execute({ skillName: 'my-skill' }, mockAbortSignal);
313+
expect(result.returnDisplay).toContain('✅ Loaded skill');
314+
});
315+
316+
it('should match skill by full ID', async () => {
317+
const mockSkill = {
318+
id: 'marketplace:plugin:my-skill',
319+
name: 'my-skill',
320+
description: 'Test skill',
321+
pluginId: 'marketplace:plugin',
322+
marketplaceId: 'marketplace',
323+
path: '/mock/path',
324+
skillFilePath: '/mock/path/SKILL.md',
325+
metadata: { name: 'my-skill' },
326+
enabled: true,
327+
loadLevel: SkillLoadLevel.RESOURCES,
328+
scripts: [],
329+
};
330+
331+
mockLoaderInstance = {
332+
loadEnabledSkills: () => [mockSkill],
333+
};
334+
mockInjectorInstance = {
335+
loadSkillLevel2: () => '# Test\n\nTest content',
336+
};
337+
338+
// Should match by full ID
339+
const result = await useSkillTool.execute(
340+
{ skillName: 'marketplace:plugin:my-skill' },
341+
mockAbortSignal
342+
);
343+
expect(result.returnDisplay).toContain('✅ Loaded skill');
344+
});
345+
346+
it('should provide detailed debug info when skill not found', async () => {
347+
const mockSkills = [
348+
{
349+
id: 'marketplace:plugin1:skill1',
350+
name: 'skill1',
351+
description: 'Skill 1',
352+
pluginId: 'marketplace:plugin1',
353+
marketplaceId: 'marketplace',
354+
path: '/mock/path1',
355+
skillFilePath: '/mock/path1/SKILL.md',
356+
metadata: { name: 'skill1' },
357+
enabled: true,
358+
loadLevel: SkillLoadLevel.RESOURCES,
359+
scripts: [],
360+
},
361+
{
362+
id: 'marketplace:plugin2:skill2',
363+
name: 'skill2',
364+
description: 'Skill 2',
365+
pluginId: 'marketplace:plugin2',
366+
marketplaceId: 'marketplace',
367+
path: '/mock/path2',
368+
skillFilePath: '/mock/path2/SKILL.md',
369+
metadata: { name: 'skill2' },
370+
enabled: true,
371+
loadLevel: SkillLoadLevel.RESOURCES,
372+
scripts: [],
373+
},
374+
];
375+
376+
mockLoaderInstance = {
377+
loadEnabledSkills: () => mockSkills,
378+
};
379+
380+
const result = await useSkillTool.execute({ skillName: 'nonexistent' }, mockAbortSignal);
381+
382+
// Should provide debug information
383+
expect(result.llmContent).toContain('❌ Skill "nonexistent" not found');
384+
expect(result.llmContent).toContain('📊 Debug Information');
385+
expect(result.llmContent).toContain('Total skills loaded: 2');
386+
expect(result.llmContent).toContain('Normalized search: "nonexistent"');
387+
expect(result.llmContent).toContain('By name:');
388+
expect(result.llmContent).toContain('skill1');
389+
expect(result.llmContent).toContain('skill2');
390+
expect(result.llmContent).toContain('By ID:');
391+
expect(result.llmContent).toContain('marketplace:plugin1:skill1');
392+
expect(result.llmContent).toContain('marketplace:plugin2:skill2');
393+
expect(result.llmContent).toContain('Inconsistency between list and use_skill');
394+
});
395+
});
254396
});

packages/core/src/tools/use-skill.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ To use skills, please run DeepV Code from the command line.`,
155155
// Find the skill by name
156156
const skills = await loader.loadEnabledSkills(SkillLoadLevel.RESOURCES);
157157

158+
// Debug logging for skill discovery issues
159+
if (process.env.DEBUG_SKILLS) {
160+
console.log(`[use_skill] Loaded ${skills.length} skills from SkillLoader:`);
161+
skills.forEach((s: Skill) => {
162+
console.log(` - ${s.name} (id: ${s.id}, isCustom: ${s.isCustom}, location: ${s.location?.type || 'N/A'})`);
163+
});
164+
}
165+
158166
// 更健壮的匹配逻辑:支持多种格式
159167
const normalizedSearchName = params.skillName.toLowerCase().trim();
160168
const matchingSkills = skills.filter((s: Skill) => {
@@ -178,16 +186,36 @@ To use skills, please run DeepV Code from the command line.`,
178186

179187
if (matchingSkills.length === 0) {
180188
const availableSkills = skills.map((s: Skill) => `${s.name} (id: ${s.id})`).join(', ');
189+
const availableNames = skills.map((s: Skill) => s.name).sort().join(', ');
190+
const availableIds = skills.map((s: Skill) => s.id).sort().join(', ');
191+
181192
return {
182193
llmContent: `❌ Skill "${params.skillName}" not found.
183194
184-
Available skills (${skills.length} total): ${availableSkills || 'none'}
185-
186-
Possible issues:
187-
- Skill name is incorrect (check spelling and case)
188-
- Skill is not installed (use /skill list to see all skills)
189-
- Plugin is disabled
190-
- Skill is in a different location (user-global vs project-level)
195+
📊 Debug Information:
196+
- Your search: "${params.skillName}"
197+
- Normalized search: "${normalizedSearchName}"
198+
- Total skills loaded: ${skills.length}
199+
- Loader source: SkillLoader.loadEnabledSkills()
200+
201+
📋 Available skills (${skills.length} total):
202+
By name: ${availableNames || 'none'}
203+
204+
By ID: ${availableIds || 'none'}
205+
206+
🔍 Possible issues:
207+
- Skill name is incorrect (check spelling and case-sensitivity)
208+
- Skill is not installed (use /skill list to see all skills)
209+
- Plugin is disabled (use /skill plugin list to check status)
210+
- Skill is in a different location (user-global vs project-level)
211+
- ⚠️ Inconsistency between list and use_skill (this may be a bug)
212+
213+
💡 Troubleshooting steps:
214+
1. Run: /skill list (to see all discoverable skills)
215+
2. If you can see the skill with /skill list but not here, this indicates a
216+
skill discovery inconsistency - please report this as a bug
217+
3. Check plugin status: /skill plugin list
218+
4. Try using the full skill ID instead of just the name
191219
192220
To see detailed skill information, check the "Available Skills" section in the system context.`,
193221
returnDisplay: `Skill "${params.skillName}" not found`,

packages/vscode-ui-plugin/src/extension.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2679,6 +2679,25 @@ function setupMultiSessionHandlers() {
26792679
}
26802680
});
26812681

2682+
// 🎯 处理Session拖拽排序请求
2683+
communicationService.addMessageHandler('session_reorder', async (payload: { sessionIds: string[] }) => {
2684+
try {
2685+
logger.info('Received session_reorder request', { sessionIds: payload.sessionIds.map(id => id.substring(0, 8)) });
2686+
2687+
// 调用持久化服务保存新顺序
2688+
await sessionManager.saveSessionsOrder(payload.sessionIds);
2689+
2690+
logger.info('✅ Session order saved successfully');
2691+
2692+
// 🎯 注意:不发送 session_list_update!
2693+
// 前端已经通过 reorderSessions 更新了 UI,
2694+
// 如果发送 session_list_update,会覆盖前端的拖拽顺序
2695+
// (因为 getAllSessionsInfo() 按 lastActivity 排序,不是用户自定义顺序)
2696+
} catch (error) {
2697+
logger.error('Failed to reorder sessions', error instanceof Error ? error : undefined);
2698+
}
2699+
});
2700+
26822701
// 处理Session更新请求
26832702
communicationService.onSessionUpdate(async (payload) => {
26842703
try {

packages/vscode-ui-plugin/src/services/sessionManager.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export class SessionManager extends EventEmitter {
5555
private currentSessionId: string | null = null;
5656
private isInitialized = false;
5757

58+
// 🎯 Session 顺序管理(用于拖拽排序)
59+
private sessionsOrder: string[] = [];
60+
5861
// 🎯 用户内存/上下文内容缓存(全局共享)
5962
private userMemoryContent: string = '';
6063
private userMemoryFileCount: number = 0;
@@ -140,6 +143,8 @@ export class SessionManager extends EventEmitter {
140143
const persistedSessions = await this.persistenceService.loadSessions();
141144
if (persistedSessions.length > 0) {
142145
await this.restoreSessions(persistedSessions);
146+
// 🎯 初始化 sessionsOrder(按持久化层的顺序,支持拖拽排序)
147+
this.sessionsOrder = persistedSessions.map(s => s.info.id);
143148
this.logger.info(`📦 Restored ${persistedSessions.length} persisted sessions`);
144149
} else {
145150
// 没有持久化会话,创建默认会话
@@ -460,6 +465,9 @@ export class SessionManager extends EventEmitter {
460465
this.sessions.set(sessionState.info.id, sessionState);
461466
this.aiServices.set(sessionState.info.id, aiService);
462467

468+
// 🎯 将新 session 添加到顺序列表开头(最新创建的在前)
469+
this.sessionsOrder = [sessionId, ...this.sessionsOrder];
470+
463471
// 🎯 如果需要激活,设置为当前session并将之前的session设为IDLE
464472
if (shouldActivate) {
465473
// 将之前的current session设为IDLE
@@ -532,6 +540,9 @@ export class SessionManager extends EventEmitter {
532540
this.logger.info(`🗑️ Session ${sessionId} not in memory, deleting directly from disk...`);
533541
}
534542

543+
// 🎯 从顺序列表中移除
544+
this.sessionsOrder = this.sessionsOrder.filter(id => id !== sessionId);
545+
535546
// 🎯 无论是否在内存中,都从持久化存储中删除
536547
await this.persistenceService.deleteSession(sessionId);
537548
this.logger.info(`✅ Deleted session from disk: ${sessionId}`);
@@ -738,11 +749,46 @@ export class SessionManager extends EventEmitter {
738749

739750
/**
740751
* 获取所有会话信息列表
752+
* 🎯 按用户自定义的拖拽顺序返回(如果有),否则按 lastActivity 排序
741753
*/
742754
getAllSessionsInfo(): SessionInfo[] {
743-
return Array.from(this.sessions.values())
744-
.map(session => session.info)
745-
.sort((a, b) => b.lastActivity - a.lastActivity);
755+
const allSessions = Array.from(this.sessions.values()).map(session => session.info);
756+
757+
// 🎯 如果有自定义顺序,按顺序返回
758+
if (this.sessionsOrder.length > 0) {
759+
const orderedSessions: SessionInfo[] = [];
760+
const sessionMap = new Map(allSessions.map(s => [s.id, s]));
761+
762+
// 按 sessionsOrder 顺序添加
763+
for (const id of this.sessionsOrder) {
764+
const session = sessionMap.get(id);
765+
if (session) {
766+
orderedSessions.push(session);
767+
sessionMap.delete(id);
768+
}
769+
}
770+
771+
// 添加不在 sessionsOrder 中的新 session(按 lastActivity 排序)
772+
const remainingSessions = Array.from(sessionMap.values())
773+
.sort((a, b) => b.lastActivity - a.lastActivity);
774+
orderedSessions.push(...remainingSessions);
775+
776+
return orderedSessions;
777+
}
778+
779+
// 没有自定义顺序时,按 lastActivity 排序
780+
return allSessions.sort((a, b) => b.lastActivity - a.lastActivity);
781+
}
782+
783+
/**
784+
* 🎯 保存Session顺序(用于拖拽排序)
785+
* @param sessionIds 按用户拖拽后的新顺序排列的sessionId数组
786+
*/
787+
async saveSessionsOrder(sessionIds: string[]): Promise<void> {
788+
// 🎯 同时更新内存中的顺序
789+
this.sessionsOrder = [...sessionIds];
790+
await this.persistenceService.saveSessionsOrder(sessionIds);
791+
this.logger.info(`✅ Session order saved: ${sessionIds.length} sessions`);
746792
}
747793

748794
/**

0 commit comments

Comments
 (0)