@@ -17,10 +17,10 @@ interface McpServerInfo {
1717interface AiToolsDataMessage {
1818 command : "aiToolsData" ;
1919 skills : SkillInfo [ ] ;
20- installedByIde : Record < string , string [ ] > ; // ideLabel → array of dirNames
20+ installedByPlatform : Record < string , string [ ] > ; // platformId → array of dirNames
21+ activePlatforms : string [ ] ;
2122 mcpServers : McpServerInfo [ ] ;
2223 configuredMcpKeys : string [ ] ;
23- detectedIde : string ;
2424 error ?: string ;
2525}
2626
@@ -42,7 +42,6 @@ type InboundMessage = AiToolsDataMessage | AiToolsProgressMessage | AiToolsResul
4242let _isOpen = false ;
4343let _dataFetched = false ;
4444let _cachedData : Omit < AiToolsDataMessage , "command" > | null = null ;
45- let _activeIde = "Claude Code" ;
4645
4746// ── DOM helpers ───────────────────────────────────────────────────────────────
4847
@@ -77,41 +76,87 @@ function showPanelState(state: "loading" | "ready" | "done" | "error"): void {
7776 }
7877}
7978
80- // ── IDE pill ──────── ──────────────────────────────────────────────────────────
79+ // ── Helper functions ──────────────────────────────────────────────────────────
8180
82- function movePill ( btn : HTMLElement ) : void {
83- const pill = el < HTMLElement > ( "hs-ai-ide-pill" ) ;
84- if ( ! pill ) { return ; }
85- pill . style . left = btn . offsetLeft + "px" ;
86- pill . style . width = btn . offsetWidth + "px" ;
81+ function getCheckedPlatforms ( ) : string [ ] {
82+ return [ ...document . querySelectorAll < HTMLInputElement > ( ".hs-ai-platform-cb" ) ]
83+ . filter ( ( c ) => c . checked )
84+ . map ( ( c ) => c . dataset . platform ! ) ;
85+ }
86+
87+ function computeSkillStatus (
88+ dirName : string ,
89+ installedByPlatform : Record < string , string [ ] > ,
90+ checkedPlatforms : string [ ]
91+ ) : "installed" | "partial" | "none" {
92+ if ( checkedPlatforms . length === 0 ) { return "none" ; }
93+ const installedCount = checkedPlatforms . filter (
94+ ( pid ) => ( installedByPlatform [ pid ] ?? [ ] ) . includes ( dirName )
95+ ) . length ;
96+ if ( installedCount === checkedPlatforms . length ) { return "installed" ; }
97+ if ( installedCount > 0 ) { return "partial" ; }
98+ return "none" ;
8799}
88100
89- function initPill ( ) : void {
90- const activeBtn = document . querySelector < HTMLElement > ( ".hs-ai-ide-btn.active" ) ;
91- if ( activeBtn ) { movePill ( activeBtn ) ; }
101+ // ── Platform definitions and rendering ───────────────────────────────────────
102+
103+ const PLATFORM_DEFS = [
104+ { id : "universal" , label : "Universal" , sublabel : "Cursor, Codex, Amp, Warp + more" } ,
105+ { id : "claude-code" , label : "Claude Code" } ,
106+ { id : "vscode-copilot" , label : "VS Code (Copilot)" } ,
107+ { id : "windsurf" , label : "Windsurf" } ,
108+ ] ;
109+
110+ function renderPlatformRows ( activePlatforms : string [ ] ) : void {
111+ const list = el ( "hs-ai-platform-list" ) ;
112+ if ( ! list ) { return ; }
113+ const activeSet = new Set ( activePlatforms ) ;
114+ list . innerHTML = PLATFORM_DEFS . map ( ( p ) => {
115+ const checked = activeSet . has ( p . id ) ? "checked" : "" ;
116+ const sublabel = p . sublabel
117+ ? `<span class="hs-ai-platform-sub">${ escapeHtml ( p . sublabel ) } </span>`
118+ : "" ;
119+ return `<label class="hs-ai-item hs-ai-platform-row">
120+ <input type="checkbox" class="hs-ai-cb hs-ai-platform-cb" data-platform="${ escapeHtml ( p . id ) } " ${ checked } >
121+ <span class="hs-ai-item-name">${ escapeHtml ( p . label ) } ${ sublabel } </span>
122+ </label>` ;
123+ } ) . join ( "" ) ;
124+ list . querySelectorAll < HTMLInputElement > ( ".hs-ai-platform-cb" ) . forEach ( ( cb ) => {
125+ cb . addEventListener ( "change" , onPlatformChange ) ;
126+ } ) ;
127+ }
128+
129+ function onPlatformChange ( ) : void {
130+ if ( ! _cachedData ) { return ; }
131+ const checkedPlatforms = getCheckedPlatforms ( ) ;
132+ renderSkillRows ( _cachedData . skills , _cachedData . installedByPlatform , checkedPlatforms ) ;
133+ updateApplyButton ( ) ;
92134}
93135
94136// ── Checklist rendering ───────────────────────────────────────────────────────
95137
96138function renderSkillRows (
97139 skills : SkillInfo [ ] ,
98- installedDirNames : string [ ]
140+ installedByPlatform : Record < string , string [ ] > ,
141+ checkedPlatforms : string [ ]
99142) : void {
100143 const list = el ( "hs-ai-skills-list" ) ;
101144 if ( ! list ) { return ; }
102- const installedSet = new Set ( installedDirNames ) ;
103- list . innerHTML = skills
104- . map ( ( s ) => {
105- const isInstalled = installedSet . has ( s . dirName ) ;
106- const statusClass = isInstalled ? "hs-ai-item-status--ok" : "hs-ai-item-status--none" ;
107- const statusText = isInstalled ? "installed" : "—" ;
108- return `<label class="hs-ai-item">
109- <input type="checkbox" class="hs-ai-cb" data-skill="${ escapeHtml ( s . dirName ) } " ${ isInstalled ? "" : "checked" } >
110- <span class="hs-ai-item-name" title="${ escapeHtml ( s . description ) } ">${ escapeHtml ( s . name ) } </span>
111- <span class="hs-ai-item-status ${ statusClass } ">${ statusText } </span>
112- </label>` ;
113- } )
114- . join ( "" ) ;
145+ list . innerHTML = skills . map ( ( s ) => {
146+ const status = computeSkillStatus ( s . dirName , installedByPlatform , checkedPlatforms ) ;
147+ const statusClass = status === "installed" ? "hs-ai-item-status--ok"
148+ : status === "partial" ? "hs-ai-item-status--partial"
149+ : "hs-ai-item-status--none" ;
150+ const statusText = status === "installed" ? "installed"
151+ : status === "partial" ? "partial"
152+ : "—" ;
153+ const checked = status !== "installed" ? "checked" : "" ;
154+ return `<label class="hs-ai-item">
155+ <input type="checkbox" class="hs-ai-cb" data-skill="${ escapeHtml ( s . dirName ) } " ${ checked } >
156+ <span class="hs-ai-item-name" title="${ escapeHtml ( s . description ) } ">${ escapeHtml ( s . name ) } </span>
157+ <span class="hs-ai-item-status ${ statusClass } ">${ statusText } </span>
158+ </label>` ;
159+ } ) . join ( "" ) ;
115160 list . querySelectorAll < HTMLInputElement > ( ".hs-ai-cb" ) . forEach ( ( cb ) => {
116161 cb . addEventListener ( "change" , updateApplyButton ) ;
117162 } ) ;
@@ -144,9 +189,10 @@ function renderMcpRows(
144189function updateApplyButton ( ) : void {
145190 const applyBtn = el < HTMLButtonElement > ( "hs-ai-apply" ) ;
146191 if ( ! applyBtn ) { return ; }
147- const anyChecked = [ ...document . querySelectorAll < HTMLInputElement > ( ".hs-ai-cb" ) ]
192+ const anyItemChecked = [ ...document . querySelectorAll < HTMLInputElement > ( ".hs-ai-cb[data-skill], .hs-ai-cb[data-mcp] " ) ]
148193 . some ( ( c ) => c . checked && ! c . disabled ) ;
149- applyBtn . disabled = ! anyChecked ;
194+ const anyPlatformChecked = getCheckedPlatforms ( ) . length > 0 ;
195+ applyBtn . disabled = ! ( anyItemChecked && anyPlatformChecked ) ;
150196}
151197
152198// ── Accordion toggle ──────────────────────────────────────────────────────────
@@ -168,13 +214,6 @@ function toggleAccordion(): void {
168214 showPanelState ( "loading" ) ;
169215 getVSCode ( ) ?. postMessage ( { command : "aiToolsExpanded" } ) ;
170216 }
171-
172- if ( _isOpen ) {
173- // Re-position pill after layout settles (accordion may have just opened)
174- panel . addEventListener ( "transitionend" , ( ) => {
175- initPill ( ) ;
176- } , { once : true } ) ;
177- }
178217}
179218
180219// ── Apply ─────────────────────────────────────────────────────────────────────
@@ -185,15 +224,10 @@ function handleApply(): void {
185224 const skillCheckboxes = document . querySelectorAll < HTMLInputElement > ( ".hs-ai-cb[data-skill]" ) ;
186225 const mcpCheckboxes = document . querySelectorAll < HTMLInputElement > ( ".hs-ai-cb[data-mcp]" ) ;
187226
188- const selectedSkills = [ ...skillCheckboxes ]
189- . filter ( ( c ) => c . checked )
190- . map ( ( c ) => c . dataset . skill ! ) ;
191-
192- const selectedMcpKeys = [ ...mcpCheckboxes ]
193- . filter ( ( c ) => c . checked )
194- . map ( ( c ) => c . dataset . mcp ! ) ;
227+ const selectedSkills = [ ...skillCheckboxes ] . filter ( ( c ) => c . checked ) . map ( ( c ) => c . dataset . skill ! ) ;
228+ const selectedMcpKeys = [ ...mcpCheckboxes ] . filter ( ( c ) => c . checked ) . map ( ( c ) => c . dataset . mcp ! ) ;
229+ const selectedPlatforms = getCheckedPlatforms ( ) ;
195230
196- // Switch to applying visual: disable checkboxes and apply button
197231 document . querySelectorAll < HTMLInputElement > ( ".hs-ai-cb" ) . forEach ( ( c ) => { c . disabled = true ; } ) ;
198232 const applyBtn = el < HTMLButtonElement > ( "hs-ai-apply" ) ;
199233 if ( applyBtn ) {
@@ -204,7 +238,7 @@ function handleApply(): void {
204238 getVSCode ( ) ?. postMessage ( {
205239 command : "installAiTools" ,
206240 skills : selectedSkills ,
207- ideTarget : _activeIde ,
241+ platforms : selectedPlatforms ,
208242 mcpServers : selectedMcpKeys ,
209243 } ) ;
210244}
@@ -221,26 +255,18 @@ function handleAiToolsData(msg: AiToolsDataMessage): void {
221255
222256 _cachedData = {
223257 skills : msg . skills ,
224- installedByIde : msg . installedByIde ,
258+ installedByPlatform : msg . installedByPlatform ,
259+ activePlatforms : msg . activePlatforms ,
225260 mcpServers : msg . mcpServers ,
226261 configuredMcpKeys : msg . configuredMcpKeys ,
227- detectedIde : msg . detectedIde ,
228262 } ;
229263
230- // Set active IDE to detected editor
231- _activeIde = msg . detectedIde ;
232- document . querySelectorAll < HTMLElement > ( ".hs-ai-ide-btn" ) . forEach ( ( btn ) => {
233- const isActive = btn . dataset . ide === _activeIde ;
234- btn . classList . toggle ( "active" , isActive ) ;
235- } ) ;
236-
237- renderSkillRows ( msg . skills , msg . installedByIde [ _activeIde ] ?? [ ] ) ;
264+ const checkedPlatforms = msg . activePlatforms ;
265+ renderPlatformRows ( checkedPlatforms ) ;
266+ renderSkillRows ( msg . skills , msg . installedByPlatform , checkedPlatforms ) ;
238267 renderMcpRows ( msg . mcpServers , msg . configuredMcpKeys ) ;
239-
240268 showPanelState ( "ready" ) ;
241269 updateApplyButton ( ) ;
242-
243- requestAnimationFrame ( ( ) => { initPill ( ) ; } ) ;
244270}
245271
246272function handleAiToolsProgress ( msg : AiToolsProgressMessage ) : void {
@@ -336,19 +362,6 @@ function init(): void {
336362 // Accordion toggle
337363 el ( "hs-btn-ai-tools" ) . addEventListener ( "click" , toggleAccordion ) ;
338364
339- // IDE selector buttons
340- document . querySelectorAll < HTMLElement > ( ".hs-ai-ide-btn" ) . forEach ( ( btn ) => {
341- btn . addEventListener ( "click" , ( ) => {
342- if ( ! _cachedData ) { return ; }
343- document . querySelectorAll < HTMLElement > ( ".hs-ai-ide-btn" ) . forEach ( ( b ) => b . classList . remove ( "active" ) ) ;
344- btn . classList . add ( "active" ) ;
345- movePill ( btn ) ;
346- _activeIde = btn . dataset . ide ?? "Claude Code" ;
347- renderSkillRows ( _cachedData . skills , _cachedData . installedByIde [ _activeIde ] ?? [ ] ) ;
348- updateApplyButton ( ) ;
349- } ) ;
350- } ) ;
351-
352365 // Apply button
353366 el < HTMLButtonElement > ( "hs-ai-apply" ) ?. addEventListener ( "click" , handleApply ) ;
354367
0 commit comments