From 764696a25e2f2ae81a9bb8e02ef11a6615754006 Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Sun, 14 Dec 2025 21:25:58 +0400 Subject: [PATCH] TUI5: Added scrollable Skill Selector modal - /skill and /skills now open SelectInput picker --- bin/opencode-ink.mjs | 109 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/bin/opencode-ink.mjs b/bin/opencode-ink.mjs index 19c290e..95567d7 100644 --- a/bin/opencode-ink.mjs +++ b/bin/opencode-ink.mjs @@ -2289,6 +2289,10 @@ const App = () => { const [showCommandPalette, setShowCommandPalette] = useState(false); const [paletteFilter, setPaletteFilter] = useState(''); // For search + // SKILL SELECTOR: Overlay for selecting skills + const [showSkillSelector, setShowSkillSelector] = useState(false); + const [activeSkill, setActiveSkill] = useState(null); + // PRO PROTOCOL: Run state management const [currentRun, setCurrentRun] = useState(null); @@ -2622,28 +2626,25 @@ const App = () => { return; } - case '/skills': { - const display = getSkillListDisplay(); - setMessages(prev => [...prev, { role: 'system', content: `🎯 **Available Skills**\n${display}\nUsage: /skill then describe your task` }]); - setInput(''); - return; - } - + case '/skills': case '/skill': { if (!arg) { + // Open skill selector + setShowSkillSelector(true); + setInput(''); + return; + } + // Direct skill activation with argument + const skillName = arg.split(/\s+/)[0]; + const skill = getSkill(skillName); + if (!skill) { const skills = getAllSkills(); const names = skills.map(s => s.id).join(', '); - setMessages(prev => [...prev, { role: 'system', content: `❌ Usage: /skill \nAvailable: ${names}` }]); + setMessages(prev => [...prev, { role: 'system', content: `❌ Unknown skill: "${skillName}"\nAvailable: ${names}\n\nUse /skills to see the full list.` }]); } else { - const skillName = arg.split(/\s+/)[0]; - const skill = getSkill(skillName); - if (!skill) { - const skills = getAllSkills(); - const names = skills.map(s => s.id).join(', '); - setMessages(prev => [...prev, { role: 'system', content: `❌ Unknown skill: "${skillName}"\nAvailable: ${names}` }]); - } else { - setMessages(prev => [...prev, { role: 'system', content: `🎯 **Activated: ${skill.name}**\n${skill.description}\n\nNow describe your task and I'll apply this skill.` }]); - } + // Inject skill prompt into system for next message + setActiveSkill(skill); + setMessages(prev => [...prev, { role: 'system', content: `🎯 **Activated: ${skill.name}**\n${skill.description}\n\nNow describe your task and I'll apply this skill.` }]); } setInput(''); return; @@ -4140,6 +4141,80 @@ This gives the user a chance to refine requirements before implementation. ); } + // ═══════════════════════════════════════════════════════════════ + // SKILL SELECTOR OVERLAY - Scrollable skill picker + // ═══════════════════════════════════════════════════════════════ + if (showSkillSelector && appState === 'chat') { + const skills = getAllSkills(); + const skillItems = skills.map(skill => ({ + label: `${getCategoryEmoji(skill.category)} ${skill.id.padEnd(20)} ${skill.name}`, + value: skill.id, + skill: skill + })); + + // Category emoji helper + function getCategoryEmoji(cat) { + const emojis = { + design: '🎨', + documents: 'πŸ“„', + development: 'πŸ’»', + testing: 'πŸ§ͺ', + writing: '✍️', + creative: '🎭', + documentation: 'πŸ“š', + meta: 'πŸ”§' + }; + return emojis[cat] || 'πŸ“Œ'; + } + + const handleSkillSelect = (item) => { + setShowSkillSelector(false); + setActiveSkill(item.skill); + setMessages(prev => [...prev, { + role: 'system', + content: `🎯 **Activated: ${item.skill.name}**\n${item.skill.description}\n\nNow describe your task and I'll apply this skill.` + }]); + }; + + // Handle ESC to close + useInput((input, key) => { + if (key.escape) { + setShowSkillSelector(false); + } + }, { isActive: showSkillSelector }); + + return h(Box, { + flexDirection: 'column', + borderStyle: 'round', + borderColor: 'magenta', + padding: 1, + width: Math.min(55, columns - 4), + }, + // Header + h(Text, { color: 'magenta', bold: true }, '🎯 Select a Skill'), + h(Text, { color: 'gray', dimColor: true }, 'Use ↑↓ to navigate, Enter to select'), + + // Skill list with SelectInput + h(Box, { flexDirection: 'column', marginTop: 1, height: Math.min(18, rows - 8) }, + h(SelectInput, { + items: skillItems, + onSelect: handleSkillSelect, + itemComponent: ({ isSelected, label }) => + h(Text, { + color: isSelected ? 'magenta' : 'white', + bold: isSelected + }, isSelected ? `❯ ${label}` : ` ${label}`) + }) + ), + + // Footer with categories + h(Box, { marginTop: 1, flexDirection: 'column' }, + h(Text, { dimColor: true }, 'Categories: 🎨Design πŸ“„Docs πŸ’»Dev πŸ§ͺTest ✍️Write'), + h(Text, { dimColor: true }, 'Esc to close') + ) + ); + } + // ═══════════════════════════════════════════════════════════════ // COMMAND PALETTE OVERLAY (Ctrl+K) - Searchable commands // ═══════════════════════════════════════════════════════════════