Skip to content

Commit 596ae4c

Browse files
committed
feat: simplify to primary platform status with optional additional platforms
1 parent 24cfeac commit 596ae4c

2 files changed

Lines changed: 69 additions & 87 deletions

File tree

src/webview/client/homescreen.ts

Lines changed: 42 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,19 @@ interface McpServerInfo {
1414
description: string;
1515
}
1616

17+
interface AdditionalPlatform {
18+
id: string;
19+
label: string;
20+
sublabel?: string;
21+
locked: boolean;
22+
}
23+
1724
interface AiToolsDataMessage {
1825
command: "aiToolsData";
1926
skills: SkillInfo[];
20-
installedByPlatform: Record<string, string[]>; // platformId → array of dirNames
21-
activePlatforms: string[];
27+
primaryPlatform: string;
28+
installedOnPrimary: string[];
29+
additionalPlatforms: AdditionalPlatform[];
2230
mcpServers: McpServerInfo[];
2331
configuredMcpKeys: string[];
2432
error?: string;
@@ -78,87 +86,44 @@ function showPanelState(state: "loading" | "ready" | "done" | "error"): void {
7886

7987
// ── Helper functions ──────────────────────────────────────────────────────────
8088

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";
99-
}
100-
101-
// ── Platform definitions and rendering ───────────────────────────────────────
89+
// ── Platform rendering ────────────────────────────────────────────────────────
10290

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-
];
91+
const PLATFORM_LABELS: Record<string, string> = {
92+
"universal": "Universal",
93+
"claude-code": "Claude Code",
94+
"vscode-copilot": "VS Code (Copilot)",
95+
"windsurf": "Windsurf",
96+
};
10997

110-
function renderPlatformRows(
111-
activePlatforms: string[],
112-
installedByPlatform: Record<string, string[]>
113-
): void {
98+
function renderAdditionalPlatformRows(platforms: AdditionalPlatform[]): void {
11499
const list = el("hs-ai-platform-list");
115100
if (!list) { return; }
116-
const activeSet = new Set(activePlatforms);
117-
list.innerHTML = PLATFORM_DEFS.map((p) => {
118-
const hasInstalls = (installedByPlatform[p.id] ?? []).length > 0;
119-
const checked = activeSet.has(p.id) ? "checked" : "";
120-
const disabled = hasInstalls ? "disabled" : "";
101+
list.innerHTML = platforms.map((p) => {
121102
const sublabel = p.sublabel
122103
? `<span class="hs-ai-platform-sub">${escapeHtml(p.sublabel)}</span>`
123104
: "";
124105
return `<label class="hs-ai-item hs-ai-platform-row">
125-
<input type="checkbox" class="hs-ai-cb hs-ai-platform-cb" data-platform="${escapeHtml(p.id)}" ${checked} ${disabled}>
106+
<input type="checkbox" class="hs-ai-cb hs-ai-platform-cb" data-platform="${escapeHtml(p.id)}" ${p.locked ? "checked disabled" : ""}>
126107
<span class="hs-ai-item-name">${escapeHtml(p.label)}${sublabel}</span>
127108
</label>`;
128109
}).join("");
129110
list.querySelectorAll<HTMLInputElement>(".hs-ai-platform-cb").forEach((cb) => {
130-
cb.addEventListener("change", onPlatformChange);
111+
cb.addEventListener("change", updateApplyButton);
131112
});
132113
}
133114

134-
function onPlatformChange(): void {
135-
if (!_cachedData) { return; }
136-
const checkedPlatforms = getCheckedPlatforms();
137-
renderSkillRows(_cachedData.skills, _cachedData.installedByPlatform, checkedPlatforms);
138-
updateApplyButton();
139-
}
140-
141115
// ── Checklist rendering ───────────────────────────────────────────────────────
142116

143-
function renderSkillRows(
144-
skills: SkillInfo[],
145-
installedByPlatform: Record<string, string[]>,
146-
checkedPlatforms: string[]
147-
): void {
117+
function renderSkillRows(skills: SkillInfo[], installedOnPrimary: string[]): void {
148118
const list = el("hs-ai-skills-list");
149119
if (!list) { return; }
120+
const installedSet = new Set(installedOnPrimary);
150121
list.innerHTML = skills.map((s) => {
151-
const status = computeSkillStatus(s.dirName, installedByPlatform, checkedPlatforms);
152-
const statusClass = status === "installed" ? "hs-ai-item-status--ok"
153-
: status === "partial" ? "hs-ai-item-status--partial"
154-
: "hs-ai-item-status--none";
155-
const statusText = status === "installed" ? "installed"
156-
: status === "partial" ? "partial"
157-
: "—";
158-
const checked = "checked";
159-
const disabled = status === "installed" ? "disabled" : "";
122+
const isInstalled = installedSet.has(s.dirName);
123+
const statusClass = isInstalled ? "hs-ai-item-status--ok" : "hs-ai-item-status--none";
124+
const statusText = isInstalled ? "installed" : "—";
160125
return `<label class="hs-ai-item">
161-
<input type="checkbox" class="hs-ai-cb" data-skill="${escapeHtml(s.dirName)}" ${checked} ${disabled}>
126+
<input type="checkbox" class="hs-ai-cb" data-skill="${escapeHtml(s.dirName)}" ${isInstalled ? "checked disabled" : ""}>
162127
<span class="hs-ai-item-name" title="${escapeHtml(s.description)}">${escapeHtml(s.name)}</span>
163128
<span class="hs-ai-item-status ${statusClass}">${statusText}</span>
164129
</label>`;
@@ -195,10 +160,9 @@ function renderMcpRows(
195160
function updateApplyButton(): void {
196161
const applyBtn = el<HTMLButtonElement>("hs-ai-apply");
197162
if (!applyBtn) { return; }
198-
const anyItemChecked = [...document.querySelectorAll<HTMLInputElement>(".hs-ai-cb[data-skill], .hs-ai-cb[data-mcp]")]
163+
const anyActionable = [...document.querySelectorAll<HTMLInputElement>(".hs-ai-cb")]
199164
.some((c) => c.checked && !c.disabled);
200-
const anyPlatformChecked = getCheckedPlatforms().length > 0;
201-
applyBtn.disabled = !(anyItemChecked && anyPlatformChecked);
165+
applyBtn.disabled = !anyActionable;
202166
}
203167

204168
// ── Accordion toggle ──────────────────────────────────────────────────────────
@@ -230,9 +194,12 @@ function handleApply(): void {
230194
const skillCheckboxes = document.querySelectorAll<HTMLInputElement>(".hs-ai-cb[data-skill]");
231195
const mcpCheckboxes = document.querySelectorAll<HTMLInputElement>(".hs-ai-cb[data-mcp]");
232196

197+
const platformCheckboxes = document.querySelectorAll<HTMLInputElement>(".hs-ai-cb[data-platform]");
198+
233199
const selectedSkills = [...skillCheckboxes].filter((c) => c.checked).map((c) => c.dataset.skill!);
234200
const selectedMcpKeys = [...mcpCheckboxes].filter((c) => c.checked).map((c) => c.dataset.mcp!);
235-
const selectedPlatforms = getCheckedPlatforms();
201+
const additionalPlatforms = [...platformCheckboxes].filter((c) => c.checked).map((c) => c.dataset.platform!);
202+
const platforms = [_cachedData.primaryPlatform, ...additionalPlatforms];
236203

237204
document.querySelectorAll<HTMLInputElement>(".hs-ai-cb").forEach((c) => { c.disabled = true; });
238205
const applyBtn = el<HTMLButtonElement>("hs-ai-apply");
@@ -244,7 +211,7 @@ function handleApply(): void {
244211
getVSCode()?.postMessage({
245212
command: "installAiTools",
246213
skills: selectedSkills,
247-
platforms: selectedPlatforms,
214+
platforms,
248215
mcpServers: selectedMcpKeys,
249216
});
250217
}
@@ -261,15 +228,18 @@ function handleAiToolsData(msg: AiToolsDataMessage): void {
261228

262229
_cachedData = {
263230
skills: msg.skills,
264-
installedByPlatform: msg.installedByPlatform,
265-
activePlatforms: msg.activePlatforms,
231+
primaryPlatform: msg.primaryPlatform,
232+
installedOnPrimary: msg.installedOnPrimary,
233+
additionalPlatforms: msg.additionalPlatforms,
266234
mcpServers: msg.mcpServers,
267235
configuredMcpKeys: msg.configuredMcpKeys,
268236
};
269237

270-
const checkedPlatforms = msg.activePlatforms;
271-
renderPlatformRows(checkedPlatforms, msg.installedByPlatform);
272-
renderSkillRows(msg.skills, msg.installedByPlatform, checkedPlatforms);
238+
const platformLabel = el("hs-ai-skills-platform");
239+
if (platformLabel) { platformLabel.textContent = `(${PLATFORM_LABELS[msg.primaryPlatform] ?? msg.primaryPlatform})`; }
240+
241+
renderAdditionalPlatformRows(msg.additionalPlatforms);
242+
renderSkillRows(msg.skills, msg.installedOnPrimary);
273243
renderMcpRows(msg.mcpServers, msg.configuredMcpKeys);
274244
showPanelState("ready");
275245
updateApplyButton();

src/webview/homescreenView.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createWebviewDocument, getScriptUri } from "./webviewUtils";
99
import { escapeHtml } from "./utils/helpers";
1010
import {
1111
PlatformId,
12+
PLATFORMS,
1213
SkillInfo,
1314
MCP_SERVERS,
1415
detectEditor,
@@ -23,6 +24,7 @@ import {
2324
installForWindsurf,
2425
installMcpServers,
2526
detectActivePlatforms,
27+
detectEditorPlatform,
2628
} from "../aiToolsService";
2729

2830
export class HomescreenViewProvider implements vscode.WebviewViewProvider {
@@ -549,7 +551,12 @@ export class HomescreenViewProvider implements vscode.WebviewViewProvider {
549551
}
550552
.hs-ai-item-status--ok::before { background: #4ade80; }
551553
.hs-ai-item-status--none::before { background: rgba(255,255,255,0.15); }
552-
.hs-ai-item-status--partial::before { background: rgba(250,204,21,0.7); }
554+
.hs-ai-platform-badge {
555+
font-size: 9px;
556+
font-weight: 400;
557+
color: var(--vscode-descriptionForeground);
558+
margin-left: 4px;
559+
}
553560
.hs-ai-platform-sub {
554561
display: block;
555562
font-size: 9px;
@@ -711,10 +718,10 @@ export class HomescreenViewProvider implements vscode.WebviewViewProvider {
711718
<!-- Ready / applying state -->
712719
<div class="hs-ai-panel-inner hidden" id="hs-ai-state-ready">
713720
<div>
714-
<div class="hs-ai-section-head">Skills</div>
721+
<div class="hs-ai-section-head">Skills <span class="hs-ai-platform-badge" id="hs-ai-skills-platform"></span></div>
715722
<div id="hs-ai-skills-list"></div>
716723
<div class="hs-ai-hint">Installed skills are locked. Delete files to uninstall.</div>
717-
<div class="hs-ai-section-head" style="margin-top:8px">Install for</div>
724+
<div class="hs-ai-section-head" style="margin-top:8px">Also install for</div>
718725
<div id="hs-ai-platform-list"></div>
719726
</div>
720727
<div>
@@ -770,16 +777,20 @@ export class HomescreenViewProvider implements vscode.WebviewViewProvider {
770777
}
771778
const skills = this._cachedSkills;
772779

773-
const platformIds: PlatformId[] = ["universal", "claude-code", "vscode-copilot", "windsurf"];
774-
const installedByPlatform: Record<string, string[]> = {};
775-
await Promise.all(
776-
platformIds.map(async (pid) => {
777-
const set = await readInstalledSkillDirNames(rootUri, pid, skills);
778-
installedByPlatform[pid] = [...set];
779-
})
780-
);
781-
782-
const activePlatforms = await detectActivePlatforms(rootUri);
780+
const primaryPlatform = detectEditorPlatform();
781+
const [installedOnPrimarySet, activePlatforms] = await Promise.all([
782+
readInstalledSkillDirNames(rootUri, primaryPlatform, skills),
783+
detectActivePlatforms(rootUri),
784+
]);
785+
786+
const additionalPlatforms = PLATFORMS
787+
.filter((p) => p.id !== primaryPlatform)
788+
.map((p) => ({
789+
id: p.id,
790+
label: p.label,
791+
sublabel: p.sublabel,
792+
locked: activePlatforms.includes(p.id as PlatformId),
793+
}));
783794

784795
const editor = detectEditor();
785796
const mcpFilePath = getMcpFilePath(editor);
@@ -789,8 +800,9 @@ export class HomescreenViewProvider implements vscode.WebviewViewProvider {
789800
view.webview.postMessage({
790801
command: "aiToolsData",
791802
skills: skills.map((s) => ({ name: s.name, description: s.description, dirName: s.dirName })),
792-
installedByPlatform,
793-
activePlatforms,
803+
primaryPlatform,
804+
installedOnPrimary: [...installedOnPrimarySet],
805+
additionalPlatforms,
794806
mcpServers: MCP_SERVERS.map((s) => ({ key: s.key, label: s.label, description: s.description })),
795807
configuredMcpKeys: [...configuredMcpSet],
796808
});

0 commit comments

Comments
 (0)