Skip to content

Commit 39a1a0c

Browse files
aaronpowellCopilot
andauthored
Update skill modal ZIP download (#1015)
Make the skill modal download button reuse the existing skill ZIP behavior so it downloads the full skill bundle instead of only the current file. Extract the ZIP creation into a shared utility and reuse it from the skills and hooks pages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 446f0d7 commit 39a1a0c

4 files changed

Lines changed: 95 additions & 85 deletions

File tree

website/src/scripts/modal.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
copyToClipboard,
1111
showToast,
1212
downloadFile,
13+
downloadZipBundle,
1314
shareFile,
1415
getResourceType,
1516
escapeHtml,
@@ -46,6 +47,7 @@ interface SkillFile {
4647
}
4748

4849
interface SkillItem extends ResourceItem {
50+
id: string;
4951
skillFile: string;
5052
files: SkillFile[];
5153
}
@@ -56,6 +58,10 @@ interface SkillsData {
5658

5759
let skillsCache: SkillsData | null | undefined;
5860

61+
function getSkillDownloadName(skill: SkillItem): string {
62+
return skill.id || skill.path.split("/").pop() || "skill";
63+
}
64+
5965
const RESOURCE_TYPE_TO_JSON: Record<string, string> = {
6066
agent: "agents.json",
6167
instruction: "instructions.json",
@@ -524,6 +530,24 @@ export function setupModal(): void {
524530

525531
downloadBtn?.addEventListener("click", async () => {
526532
if (currentFilePath) {
533+
if (currentFileType === "skill") {
534+
const skill = await getSkillItemByFilePath(currentFilePath);
535+
if (!skill || skill.files.length === 0) {
536+
showToast("No files found for this skill.", "error");
537+
return;
538+
}
539+
540+
try {
541+
await downloadZipBundle(getSkillDownloadName(skill), skill.files);
542+
showToast("Download started!", "success");
543+
} catch (error) {
544+
const message =
545+
error instanceof Error ? error.message : "Download failed";
546+
showToast(message, "error");
547+
}
548+
return;
549+
}
550+
527551
const success = await downloadFile(currentFilePath);
528552
showToast(
529553
success ? "Download started!" : "Download failed",
@@ -868,6 +892,12 @@ export async function openFileModal(
868892
// Show copy/download buttons for regular files
869893
if (copyBtn) copyBtn.style.display = "inline-flex";
870894
if (downloadBtn) downloadBtn.style.display = "inline-flex";
895+
if (downloadBtn) {
896+
downloadBtn.setAttribute(
897+
"aria-label",
898+
type === "skill" ? "Download skill as ZIP" : "Download file"
899+
);
900+
}
871901
renderPlainText("Loading...");
872902
hideSkillFileSwitcher();
873903
updateViewButtons();

website/src/scripts/pages/hooks.ts

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { FuzzySearch, type SearchItem } from "../search";
66
import {
77
fetchData,
88
debounce,
9-
getRawGitHubUrl,
109
showToast,
11-
loadJSZip,
10+
downloadZipBundle,
1211
} from "../utils";
1312
import { setupModal, openFileModal } from "../modal";
1413
import {
@@ -157,42 +156,7 @@ async function downloadHook(
157156
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
158157

159158
try {
160-
const JSZip = await loadJSZip();
161-
const zip = new JSZip();
162-
const folder = zip.folder(hook.id);
163-
164-
const fetchPromises = files.map(async (file) => {
165-
const url = getRawGitHubUrl(file.path);
166-
try {
167-
const response = await fetch(url);
168-
if (!response.ok) return null;
169-
const content = await response.text();
170-
return { name: file.name, content };
171-
} catch {
172-
return null;
173-
}
174-
});
175-
176-
const results = await Promise.all(fetchPromises);
177-
let addedFiles = 0;
178-
for (const result of results) {
179-
if (result && folder) {
180-
folder.file(result.name, result.content);
181-
addedFiles++;
182-
}
183-
}
184-
185-
if (addedFiles === 0) throw new Error("Failed to fetch any files");
186-
187-
const blob = await zip.generateAsync({ type: "blob" });
188-
const downloadUrl = URL.createObjectURL(blob);
189-
const link = document.createElement("a");
190-
link.href = downloadUrl;
191-
link.download = `${hook.id}.zip`;
192-
document.body.appendChild(link);
193-
link.click();
194-
document.body.removeChild(link);
195-
URL.revokeObjectURL(downloadUrl);
159+
await downloadZipBundle(hook.id, files);
196160

197161
btn.innerHTML =
198162
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';

website/src/scripts/pages/skills.ts

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { FuzzySearch, type SearchItem } from "../search";
66
import {
77
fetchData,
88
debounce,
9-
getRawGitHubUrl,
109
showToast,
11-
loadJSZip,
10+
downloadZipBundle,
1211
} from "../utils";
1312
import { setupModal, openFileModal } from "../modal";
1413
import {
@@ -137,42 +136,7 @@ async function downloadSkill(
137136
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
138137

139138
try {
140-
const JSZip = await loadJSZip();
141-
const zip = new JSZip();
142-
const folder = zip.folder(skill.id);
143-
144-
const fetchPromises = skill.files.map(async (file) => {
145-
const url = getRawGitHubUrl(file.path);
146-
try {
147-
const response = await fetch(url);
148-
if (!response.ok) return null;
149-
const content = await response.text();
150-
return { name: file.name, content };
151-
} catch {
152-
return null;
153-
}
154-
});
155-
156-
const results = await Promise.all(fetchPromises);
157-
let addedFiles = 0;
158-
for (const result of results) {
159-
if (result && folder) {
160-
folder.file(result.name, result.content);
161-
addedFiles++;
162-
}
163-
}
164-
165-
if (addedFiles === 0) throw new Error("Failed to fetch any files");
166-
167-
const blob = await zip.generateAsync({ type: "blob" });
168-
const downloadUrl = URL.createObjectURL(blob);
169-
const link = document.createElement("a");
170-
link.href = downloadUrl;
171-
link.download = `${skill.id}.zip`;
172-
document.body.appendChild(link);
173-
link.click();
174-
document.body.removeChild(link);
175-
URL.revokeObjectURL(downloadUrl);
139+
await downloadZipBundle(skill.id, skill.files);
176140

177141
btn.innerHTML =
178142
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';

website/src/scripts/utils.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,66 @@ export async function loadJSZip() {
7070
return JSZip;
7171
}
7272

73+
export interface ZipDownloadFile {
74+
name: string;
75+
path: string;
76+
}
77+
78+
function triggerBlobDownload(blob: Blob, filename: string): void {
79+
const url = URL.createObjectURL(blob);
80+
const link = document.createElement("a");
81+
link.href = url;
82+
link.download = filename;
83+
document.body.appendChild(link);
84+
link.click();
85+
document.body.removeChild(link);
86+
URL.revokeObjectURL(url);
87+
}
88+
89+
export async function downloadZipBundle(
90+
bundleName: string,
91+
files: ZipDownloadFile[]
92+
): Promise<void> {
93+
if (files.length === 0) {
94+
throw new Error("No files found for this download.");
95+
}
96+
97+
const JSZip = await loadJSZip();
98+
const zip = new JSZip();
99+
const folder = zip.folder(bundleName);
100+
101+
const fetchPromises = files.map(async (file) => {
102+
try {
103+
const response = await fetch(getRawGitHubUrl(file.path));
104+
if (!response.ok) return null;
105+
106+
return {
107+
name: file.name,
108+
content: await response.text(),
109+
};
110+
} catch {
111+
return null;
112+
}
113+
});
114+
115+
const results = await Promise.all(fetchPromises);
116+
let addedFiles = 0;
117+
118+
for (const result of results) {
119+
if (result && folder) {
120+
folder.file(result.name, result.content);
121+
addedFiles++;
122+
}
123+
}
124+
125+
if (addedFiles === 0) {
126+
throw new Error("Failed to fetch any files");
127+
}
128+
129+
const blob = await zip.generateAsync({ type: "blob" });
130+
triggerBlobDownload(blob, `${bundleName}.zip`);
131+
}
132+
73133
/**
74134
* Fetch raw file content from GitHub
75135
*/
@@ -156,15 +216,7 @@ export async function downloadFile(filePath: string): Promise<boolean> {
156216
const filename = filePath.split("/").pop() || "file.md";
157217

158218
const blob = new Blob([content], { type: "text/markdown" });
159-
const url = URL.createObjectURL(blob);
160-
161-
const a = document.createElement("a");
162-
a.href = url;
163-
a.download = filename;
164-
document.body.appendChild(a);
165-
a.click();
166-
document.body.removeChild(a);
167-
URL.revokeObjectURL(url);
219+
triggerBlobDownload(blob, filename);
168220

169221
return true;
170222
} catch (error) {

0 commit comments

Comments
 (0)