Skip to content

Commit 7db2205

Browse files
committed
feat: replace IDE selector with multi-platform checklist in homescreen client
1 parent 31e94c4 commit 7db2205

1 file changed

Lines changed: 83 additions & 70 deletions

File tree

src/webview/client/homescreen.ts

Lines changed: 83 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ interface McpServerInfo {
1717
interface 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
4242
let _isOpen = false;
4343
let _dataFetched = false;
4444
let _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

96138
function 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(
144189
function 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

246272
function 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

Comments
 (0)