fix: use opencode skills as source of truth

This commit is contained in:
Bohdan Triapitsyn
2026-05-17 14:51:25 +03:00
parent 8900b8f0f2
commit bbc297202e
20 changed files with 384 additions and 101 deletions
@@ -54,6 +54,7 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
import { useGitBranches, useGitStore, useIsGitRepo } from '@/stores/useGitStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { createWorktreeDraft } from '@/lib/worktreeSessionCreator';
import { buildSessionTargetOptions } from '@/sync/session-worktree-contract';
@@ -71,6 +72,7 @@ const MAX_VISIBLE_TEXTAREA_LINES = 8;
const EMPTY_QUEUE: QueuedMessage[] = [];
const EMPTY_MESSAGES: Message[] = [];
const FILE_MENTION_TOKEN = /^@[^\s]+$/;
const INLINE_SKILL_TOKEN_PATTERN = /(^|\s)\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)/g;
const CHAT_DRAFT_PERSIST_DEBOUNCE_MS = 500;
const VS_CODE_DROP_DATA_TYPES = [
'CodeFiles',
@@ -81,6 +83,28 @@ const VS_CODE_DROP_DATA_TYPES = [
'text/plain',
];
const collectInlineSkillMentions = (text: string, skillNames: Set<string>): string[] => {
const mentions: string[] = [];
INLINE_SKILL_TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = INLINE_SKILL_TOKEN_PATTERN.exec(text)) !== null) {
const prefix = match[1] || '';
const name = match[2] || '';
const slashIndex = match.index + prefix.length;
if (slashIndex === 0 || !skillNames.has(name) || mentions.includes(name)) {
continue;
}
mentions.push(name);
}
return mentions;
};
const buildSkillMentionInstruction = (skillNames: string[]): string | null => {
if (skillNames.length === 0) return null;
const formatted = skillNames.map((name) => `/${name}`).join(', ');
return `The user explicitly mentioned these skills in their message: ${formatted}. Use the corresponding skill tool when it is relevant to accomplishing the user's request.`;
};
const hasUserMessages = (sessionId: string, directory?: string) => {
return getSyncMessages(sessionId, directory).some((message) => message.role === 'user');
};
@@ -1508,6 +1532,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
let primaryAttachments: AttachedFile[] = [];
let agentMentionName: string | undefined;
const additionalParts: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }> = [];
const availableSkillNames = new Set(useSkillsStore.getState().skills.map((skill) => skill.name));
const mentionedSkillNames: string[] = [];
const addMentionedSkills = (text: string) => {
for (const name of collectInlineSkillMentions(text, availableSkillNames)) {
if (!mentionedSkillNames.includes(name)) mentionedSkillNames.push(name);
}
};
// Consume any pending synthetic parts (from conflict resolution, etc.)
const syntheticParts = consumePendingSyntheticParts();
@@ -1517,6 +1548,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const queuedMsg = queuedMessages[i];
const { sanitizedText, mention } = parseAgentMentions(queuedMsg.content, agents);
const { sanitizedText: queuedText, attachments: mentionAttachments } = extractInlineFileMentions(sanitizedText);
addMentionedSkills(queuedText);
// Use agent mention from first message that has one
if (!agentMentionName && mention?.name) {
@@ -1546,6 +1578,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const { sanitizedText, mention } = parseAgentMentions(messageToSend, agents);
const { sanitizedText: messageText, attachments: mentionAttachments } = extractInlineFileMentions(sanitizedText);
const attachmentsToSend = sanitizeAttachmentsForSend(sendableAttachedFiles);
addMentionedSkills(messageText);
if (!agentMentionName && mention?.name) {
agentMentionName = mention.name;
@@ -1611,6 +1644,14 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
});
}
const skillMentionInstruction = buildSkillMentionInstruction(mentionedSkillNames);
if (skillMentionInstruction) {
additionalParts.push({
text: skillMentionInstruction,
synthetic: true,
});
}
if (!primaryText && additionalParts.length === 0) return;
// Clear queue and input