mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-17 21:21:20 +00:00
chore: publish from staged
This commit is contained in:
@@ -42,7 +42,8 @@ jobs:
|
|||||||
if (parts[0] === EXTENSIONS_DIR && parts.length >= 2) {
|
if (parts[0] === EXTENSIONS_DIR && parts.length >= 2) {
|
||||||
const extName = parts[1];
|
const extName = parts[1];
|
||||||
// Skip the external-assets directory — it's not a canvas extension
|
// Skip the external-assets directory — it's not a canvas extension
|
||||||
if (extName !== EXTERNAL_ASSETS_DIR) {
|
// Also skip external.json and other files at extensions root level
|
||||||
|
if (extName !== EXTERNAL_ASSETS_DIR && !extName.includes('.')) {
|
||||||
changedExtDirs.add(path.join(EXTENSIONS_DIR, extName));
|
changedExtDirs.add(path.join(EXTENSIONS_DIR, extName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+452
-69
@@ -674,11 +674,33 @@ function generatePluginsData(gitDates) {
|
|||||||
/**
|
/**
|
||||||
* Generate canvas extensions metadata
|
* Generate canvas extensions metadata
|
||||||
*/
|
*/
|
||||||
function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
function getImageMimeType(filePath) {
|
||||||
|
const extension = path.extname(filePath).toLowerCase();
|
||||||
|
const mimeByExtension = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".gif": "image/gif",
|
||||||
|
};
|
||||||
|
return mimeByExtension[extension] || "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveImageUrl(value, ref) {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (!normalized) return null;
|
||||||
|
if (/^https?:\/\//i.test(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
const repoPath = normalized.replace(/\\/g, "/").replace(/^\/+/, "");
|
||||||
|
return buildRepoImageUrl(repoPath, ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageAssetFiles(extensionDir) {
|
||||||
const assetDir = path.join(extensionDir, "assets");
|
const assetDir = path.join(extensionDir, "assets");
|
||||||
|
|
||||||
if (!fs.existsSync(assetDir)) {
|
if (!fs.existsSync(assetDir)) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageExtensions = new Set([
|
const imageExtensions = new Set([
|
||||||
@@ -689,7 +711,35 @@ function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
|||||||
".gif",
|
".gif",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const preferredNames = [
|
return fs
|
||||||
|
.readdirSync(assetDir)
|
||||||
|
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickAssetFile(files, preferredNames) {
|
||||||
|
const preferredLookup = new Set(preferredNames.map((name) => name.toLowerCase()));
|
||||||
|
for (const file of files) {
|
||||||
|
if (preferredLookup.has(file.toLowerCase())) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
||||||
|
const files = getImageAssetFiles(extensionDir);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconAsset = pickAssetFile(files, [
|
||||||
|
"icon.png",
|
||||||
|
"icon.jpg",
|
||||||
|
"icon.jpeg",
|
||||||
|
"icon.webp",
|
||||||
|
"icon.gif",
|
||||||
"preview.png",
|
"preview.png",
|
||||||
"preview.jpg",
|
"preview.jpg",
|
||||||
"preview.jpeg",
|
"preview.jpeg",
|
||||||
@@ -705,34 +755,52 @@ function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
|||||||
"image.jpeg",
|
"image.jpeg",
|
||||||
"image.webp",
|
"image.webp",
|
||||||
"image.gif",
|
"image.gif",
|
||||||
];
|
]);
|
||||||
|
const galleryAsset = pickAssetFile(files, [
|
||||||
|
"gallery.png",
|
||||||
|
"gallery.jpg",
|
||||||
|
"gallery.jpeg",
|
||||||
|
"gallery.webp",
|
||||||
|
"gallery.gif",
|
||||||
|
"preview.png",
|
||||||
|
"preview.jpg",
|
||||||
|
"preview.jpeg",
|
||||||
|
"preview.webp",
|
||||||
|
"preview.gif",
|
||||||
|
"screenshot.png",
|
||||||
|
"screenshot.jpg",
|
||||||
|
"screenshot.jpeg",
|
||||||
|
"screenshot.webp",
|
||||||
|
"screenshot.gif",
|
||||||
|
"image.png",
|
||||||
|
"image.jpg",
|
||||||
|
"image.jpeg",
|
||||||
|
"image.webp",
|
||||||
|
"image.gif",
|
||||||
|
]);
|
||||||
|
|
||||||
for (const candidate of preferredNames) {
|
const iconFile = iconAsset || galleryAsset;
|
||||||
const candidatePath = path.join(assetDir, candidate);
|
const galleryFile = galleryAsset || iconAsset;
|
||||||
if (fs.existsSync(candidatePath)) {
|
const iconPath = iconFile ? `${relPath}/assets/${iconFile}` : null;
|
||||||
const assetPath = `${relPath}/assets/${candidate}`;
|
const galleryPath = galleryFile ? `${relPath}/assets/${galleryFile}` : null;
|
||||||
return {
|
|
||||||
assetPath,
|
|
||||||
imageUrl: buildRepoImageUrl(assetPath, ref),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(assetDir)
|
|
||||||
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
|
|
||||||
.sort((a, b) => a.localeCompare(b));
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetFile = files[0];
|
|
||||||
const assetPath = `${relPath}/assets/${assetFile}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetPath,
|
screenshots: {
|
||||||
imageUrl: buildRepoImageUrl(assetPath, ref),
|
icon: iconPath
|
||||||
|
? {
|
||||||
|
path: iconPath,
|
||||||
|
type: getImageMimeType(iconPath),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
gallery: galleryPath
|
||||||
|
? {
|
||||||
|
path: galleryPath,
|
||||||
|
type: getImageMimeType(galleryPath),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
assetPath: iconPath,
|
||||||
|
imageUrl: iconPath ? buildRepoImageUrl(iconPath, ref) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,11 +812,174 @@ function buildRepoImageUrl(assetPath, ref) {
|
|||||||
return `https://raw.githubusercontent.com/github/awesome-copilot/${ref}/${encodedAssetPath}`;
|
return `https://raw.githubusercontent.com/github/awesome-copilot/${ref}/${encodedAssetPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateExtensionsData(gitDates, commitSha) {
|
function extractCanvasMetadataFromSource(source) {
|
||||||
const extensions = [];
|
const constants = new Map();
|
||||||
|
const constantPattern =
|
||||||
|
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|`([^`$]*)`)\s*;/g;
|
||||||
|
let constantMatch = constantPattern.exec(source);
|
||||||
|
while (constantMatch) {
|
||||||
|
const key = constantMatch[1];
|
||||||
|
const value = constantMatch[2] ?? constantMatch[3] ?? constantMatch[4] ?? "";
|
||||||
|
constants.set(key, value.replace(/\\n/g, "\n").trim());
|
||||||
|
constantMatch = constantPattern.exec(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExpression(expr) {
|
||||||
|
const trimmed = normalizeText(expr);
|
||||||
|
if (!trimmed) return null;
|
||||||
|
if (
|
||||||
|
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||||
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||||
|
) {
|
||||||
|
return trimmed
|
||||||
|
.slice(1, -1)
|
||||||
|
.replace(/\\n/g, "\n")
|
||||||
|
.replace(/\\"/g, '"')
|
||||||
|
.replace(/\\'/g, "'");
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("`") && trimmed.endsWith("`") && !trimmed.includes("${")) {
|
||||||
|
return trimmed.slice(1, -1);
|
||||||
|
}
|
||||||
|
return constants.get(trimmed) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMatchingBrace(startIndex) {
|
||||||
|
let depth = 0;
|
||||||
|
let inSingle = false;
|
||||||
|
let inDouble = false;
|
||||||
|
let inTemplate = false;
|
||||||
|
let escaped = false;
|
||||||
|
for (let i = startIndex; i < source.length; i++) {
|
||||||
|
const char = source[i];
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inDouble && !inTemplate && char === "'" && !inSingle) {
|
||||||
|
inSingle = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inSingle && char === "'") {
|
||||||
|
inSingle = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inSingle && !inTemplate && char === '"' && !inDouble) {
|
||||||
|
inDouble = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inDouble && char === '"') {
|
||||||
|
inDouble = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inSingle && !inDouble && char === "`" && !inTemplate) {
|
||||||
|
inTemplate = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inTemplate && char === "`") {
|
||||||
|
inTemplate = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inSingle || inDouble || inTemplate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "{") depth++;
|
||||||
|
if (char === "}") {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readProp(head, key) {
|
||||||
|
const pattern = new RegExp(`\\b${key}\\s*:\\s*([^,\\n]+)`);
|
||||||
|
const match = pattern.exec(head);
|
||||||
|
return resolveExpression(match?.[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvases = [];
|
||||||
|
let cursor = 0;
|
||||||
|
while (cursor < source.length) {
|
||||||
|
const createCanvasIndex = source.indexOf("createCanvas(", cursor);
|
||||||
|
if (createCanvasIndex === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const objectStart = source.indexOf("{", createCanvasIndex);
|
||||||
|
if (objectStart === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const objectEnd = findMatchingBrace(objectStart);
|
||||||
|
if (objectEnd === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const objectContent = source.slice(objectStart + 1, objectEnd);
|
||||||
|
const header = objectContent.slice(0, 1400);
|
||||||
|
const id = readProp(header, "id");
|
||||||
|
const displayName = readProp(header, "displayName");
|
||||||
|
const description = readProp(header, "description");
|
||||||
|
if (id || displayName || description) {
|
||||||
|
canvases.push({
|
||||||
|
id: id || null,
|
||||||
|
displayName: displayName || null,
|
||||||
|
description: description || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cursor = objectEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvases;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExtensionCanvasFiles(extensionDir) {
|
||||||
|
const queue = [extensionDir];
|
||||||
|
const files = [];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentDir = queue.shift();
|
||||||
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const absolutePath = path.join(currentDir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
queue.push(absolutePath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith(".mjs")) {
|
||||||
|
files.push(absolutePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files.sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExternalScreenshotRole(value, ref) {
|
||||||
|
if (!value) return null;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const type = getImageMimeType(value);
|
||||||
|
return {
|
||||||
|
path: value.replace(/\\/g, "/"),
|
||||||
|
type,
|
||||||
|
imageUrl: resolveImageUrl(value, ref),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const pathValue = normalizeText(value.path);
|
||||||
|
const urlValue = normalizeText(value.url);
|
||||||
|
if (!pathValue && !urlValue) return null;
|
||||||
|
const imagePath = pathValue ? pathValue.replace(/\\/g, "/") : null;
|
||||||
|
const type = normalizeText(value.type) || getImageMimeType(imagePath || urlValue);
|
||||||
|
const imageUrl = resolveImageUrl(urlValue || imagePath, ref);
|
||||||
|
return {
|
||||||
|
path: imagePath,
|
||||||
|
type,
|
||||||
|
imageUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCanvasManifest(gitDates, commitSha) {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
if (!fs.existsSync(EXTENSIONS_DIR)) {
|
if (!fs.existsSync(EXTENSIONS_DIR)) {
|
||||||
return { items: [] };
|
return { items: [], filters: { keywords: [] } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensionDirs = fs
|
const extensionDirs = fs
|
||||||
@@ -761,32 +992,61 @@ function generateExtensionsData(gitDates, commitSha) {
|
|||||||
"extension.mjs"
|
"extension.mjs"
|
||||||
);
|
);
|
||||||
return fs.existsSync(extensionEntryPoint);
|
return fs.existsSync(extensionEntryPoint);
|
||||||
});
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
for (const dir of extensionDirs) {
|
for (const dir of extensionDirs) {
|
||||||
const relPath = `extensions/${dir.name}`;
|
const relPath = `extensions/${dir.name}`;
|
||||||
const assetInfo = getExtensionAssetInfo(
|
const extensionDir = path.join(EXTENSIONS_DIR, dir.name);
|
||||||
path.join(EXTENSIONS_DIR, dir.name),
|
const packageJsonPath = path.join(extensionDir, "package.json");
|
||||||
relPath,
|
const packageJson = fs.existsSync(packageJsonPath)
|
||||||
commitSha
|
? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
|
||||||
);
|
: {};
|
||||||
|
const keywords = Array.isArray(packageJson.keywords)
|
||||||
|
? [...new Set(packageJson.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
|
||||||
|
: [];
|
||||||
|
const extensionDescription = normalizeText(packageJson.description, "Canvas extension");
|
||||||
|
const extensionName = normalizeText(packageJson.name, dir.name);
|
||||||
|
const extensionVersion = normalizeText(packageJson.version, "1.0.0");
|
||||||
|
const screenshots = getExtensionAssetInfo(extensionDir, relPath, commitSha);
|
||||||
|
const canvasFiles = getExtensionCanvasFiles(extensionDir);
|
||||||
|
const canvases = [];
|
||||||
|
for (const canvasFile of canvasFiles) {
|
||||||
|
const source = fs.readFileSync(canvasFile, "utf-8");
|
||||||
|
canvases.push(...extractCanvasMetadataFromSource(source));
|
||||||
|
}
|
||||||
|
const canvasEntries = canvases.length > 0
|
||||||
|
? canvases
|
||||||
|
: [{ id: dir.name, displayName: formatDisplayName(dir.name), description: extensionDescription }];
|
||||||
|
const installUrl = `https://github.com/github/awesome-copilot/tree/${commitSha}/${relPath.replace(
|
||||||
|
/\\/g,
|
||||||
|
"/"
|
||||||
|
)}`;
|
||||||
|
|
||||||
extensions.push({
|
for (const canvas of canvasEntries) {
|
||||||
id: dir.name,
|
const canvasId = normalizeText(canvas.id, dir.name);
|
||||||
name: formatDisplayName(dir.name),
|
const canvasName = normalizeText(canvas.displayName, formatDisplayName(canvasId));
|
||||||
description: "Canvas extension",
|
const canvasDescription = normalizeText(extensionDescription, canvas.description);
|
||||||
path: relPath,
|
items.push({
|
||||||
ref: commitSha,
|
id: canvasId,
|
||||||
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
|
canvasId,
|
||||||
imageUrl: assetInfo?.imageUrl || null,
|
extensionId: dir.name,
|
||||||
assetPath: assetInfo?.assetPath || null,
|
extensionName,
|
||||||
installUrl: `https://github.com/github/awesome-copilot/tree/${commitSha}/${relPath.replace(
|
name: canvasName,
|
||||||
/\\/g,
|
version: extensionVersion,
|
||||||
"/"
|
description: canvasDescription,
|
||||||
)}`,
|
path: relPath,
|
||||||
sourceUrl: null,
|
ref: commitSha,
|
||||||
external: false,
|
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
|
||||||
});
|
screenshots: screenshots?.screenshots || { icon: null, gallery: null },
|
||||||
|
imageUrl: screenshots?.imageUrl || null,
|
||||||
|
assetPath: screenshots?.assetPath || null,
|
||||||
|
installUrl,
|
||||||
|
sourceUrl: null,
|
||||||
|
external: false,
|
||||||
|
keywords,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const externalJsonPath = path.join(EXTENSIONS_DIR, "external.json");
|
const externalJsonPath = path.join(EXTENSIONS_DIR, "external.json");
|
||||||
@@ -805,27 +1065,58 @@ function generateExtensionsData(gitDates, commitSha) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = normalizeText(ext?.id || name.toLowerCase().replace(/\s+/g, "-"));
|
const id = normalizeText(ext?.id || name.toLowerCase().replace(/\s+/g, "-"));
|
||||||
let imageUrl = normalizeText(ext?.imageUrl);
|
const keywords = Array.isArray(ext?.keywords)
|
||||||
let assetPath = null;
|
? [...new Set(ext.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
|
||||||
const imagePath = normalizeText(ext?.imagePath);
|
: Array.isArray(ext?.tags)
|
||||||
if (!imageUrl && imagePath) {
|
? [...new Set(ext.tags.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
|
||||||
const repoAssetPath = imagePath.replace(/\\/g, "/");
|
: [];
|
||||||
imageUrl = buildRepoImageUrl(repoAssetPath, commitSha);
|
const iconScreenshot =
|
||||||
assetPath = repoAssetPath;
|
normalizeExternalScreenshotRole(ext?.screenshots?.icon, commitSha) ||
|
||||||
}
|
normalizeExternalScreenshotRole(ext?.iconPath, commitSha) ||
|
||||||
|
normalizeExternalScreenshotRole(ext?.imagePath, commitSha) ||
|
||||||
|
normalizeExternalScreenshotRole(ext?.iconUrl, commitSha) ||
|
||||||
|
normalizeExternalScreenshotRole(ext?.imageUrl, commitSha);
|
||||||
|
const galleryScreenshot =
|
||||||
|
normalizeExternalScreenshotRole(ext?.screenshots?.gallery, commitSha) ||
|
||||||
|
normalizeExternalScreenshotRole(ext?.galleryPath, commitSha) ||
|
||||||
|
normalizeExternalScreenshotRole(ext?.galleryUrl, commitSha) ||
|
||||||
|
iconScreenshot;
|
||||||
|
const screenshots = {
|
||||||
|
icon: iconScreenshot
|
||||||
|
? {
|
||||||
|
path: iconScreenshot.path,
|
||||||
|
type: iconScreenshot.type,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
gallery: galleryScreenshot
|
||||||
|
? {
|
||||||
|
path: galleryScreenshot.path,
|
||||||
|
type: galleryScreenshot.type,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
const imageUrl = iconScreenshot?.imageUrl || null;
|
||||||
|
const assetPath = iconScreenshot?.path || null;
|
||||||
|
const canvasId = normalizeText(ext?.canvasId, id);
|
||||||
|
|
||||||
extensions.push({
|
items.push({
|
||||||
id,
|
id,
|
||||||
|
canvasId,
|
||||||
|
extensionId: id,
|
||||||
|
extensionName: name,
|
||||||
name,
|
name,
|
||||||
|
version: normalizeText(ext?.version, "1.0.0"),
|
||||||
description: normalizeText(ext?.description, "External canvas extension"),
|
description: normalizeText(ext?.description, "External canvas extension"),
|
||||||
path: null,
|
path: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
lastUpdated: null,
|
lastUpdated: null,
|
||||||
imageUrl: imageUrl || null,
|
screenshots,
|
||||||
|
imageUrl,
|
||||||
assetPath,
|
assetPath,
|
||||||
installUrl,
|
installUrl,
|
||||||
sourceUrl: sourceUrl || null,
|
sourceUrl: sourceUrl || null,
|
||||||
external: true,
|
external: true,
|
||||||
|
keywords,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -834,11 +1125,98 @@ function generateExtensionsData(gitDates, commitSha) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedExtensions = extensions.sort((a, b) =>
|
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
a.name.localeCompare(b.name)
|
const keywordFilters = [...new Set(sortedItems.flatMap((item) => item.keywords || []))]
|
||||||
);
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
return { items: sortedExtensions };
|
return {
|
||||||
|
items: sortedItems,
|
||||||
|
filters: {
|
||||||
|
keywords: keywordFilters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateExtensionsData(canvasManifestData) {
|
||||||
|
if (!canvasManifestData || !Array.isArray(canvasManifestData.items)) {
|
||||||
|
return { items: [], filters: { keywords: [] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = canvasManifestData.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
keywords: Array.isArray(item.keywords) ? item.keywords : [],
|
||||||
|
screenshots: item.screenshots || { icon: null, gallery: null },
|
||||||
|
}));
|
||||||
|
const filters = {
|
||||||
|
keywords: [...new Set(items.flatMap((item) => item.keywords))]
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => a.localeCompare(b)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { items, filters };
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePerExtensionCanvasManifests(canvasManifestData) {
|
||||||
|
const manifests = new Map();
|
||||||
|
|
||||||
|
function toExtensionRelativePath(assetPath, extensionId) {
|
||||||
|
const normalizedPath = normalizeText(assetPath).replace(/\\/g, "/");
|
||||||
|
if (!normalizedPath) return null;
|
||||||
|
const prefix = `extensions/${extensionId}/`;
|
||||||
|
return normalizedPath.startsWith(prefix)
|
||||||
|
? normalizedPath.slice(prefix.length)
|
||||||
|
: normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRelativeScreenshots(screenshots, extensionId) {
|
||||||
|
if (!screenshots) return { icon: null, gallery: null };
|
||||||
|
const toRelativeEntry = (entry) =>
|
||||||
|
entry
|
||||||
|
? {
|
||||||
|
...entry,
|
||||||
|
path: toExtensionRelativePath(entry.path, extensionId),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
icon: toRelativeEntry(screenshots.icon),
|
||||||
|
gallery: toRelativeEntry(screenshots.gallery),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of canvasManifestData.items || []) {
|
||||||
|
if (!item || item.external || !item.extensionId || !item.path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume one canvas per extension folder.
|
||||||
|
if (manifests.has(item.extensionId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests.set(item.extensionId, {
|
||||||
|
id: item.canvasId || item.id,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description || "Canvas extension",
|
||||||
|
version: item.version || "1.0.0",
|
||||||
|
keywords: Array.isArray(item.keywords)
|
||||||
|
? [...new Set(item.keywords)].sort((a, b) => a.localeCompare(b))
|
||||||
|
: [],
|
||||||
|
screenshots: toRelativeScreenshots(
|
||||||
|
item.screenshots || { icon: null, gallery: null },
|
||||||
|
item.extensionId
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [extensionId, manifest] of manifests.entries()) {
|
||||||
|
const canvasManifestPath = path.join(
|
||||||
|
EXTENSIONS_DIR,
|
||||||
|
extensionId,
|
||||||
|
"canvas.json"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(canvasManifestPath, JSON.stringify(manifest, null, 2));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1181,9 +1559,12 @@ async function main() {
|
|||||||
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
|
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
|
||||||
);
|
);
|
||||||
|
|
||||||
const extensionsData = generateExtensionsData(gitDates, commitSha);
|
const canvasManifestData = generateCanvasManifest(gitDates, commitSha);
|
||||||
|
const extensionsData = generateExtensionsData(canvasManifestData);
|
||||||
const extensions = extensionsData.items;
|
const extensions = extensionsData.items;
|
||||||
console.log(`✓ Generated ${extensions.length} extensions`);
|
console.log(
|
||||||
|
`✓ Generated ${extensions.length} extensions (${extensionsData.filters.keywords.length} keywords)`
|
||||||
|
);
|
||||||
|
|
||||||
const toolsData = generateToolsData();
|
const toolsData = generateToolsData();
|
||||||
const tools = toolsData.items;
|
const tools = toolsData.items;
|
||||||
@@ -1248,6 +1629,8 @@ async function main() {
|
|||||||
JSON.stringify(extensionsData, null, 2)
|
JSON.stringify(extensionsData, null, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
writePerExtensionCanvasManifests(canvasManifestData);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(WEBSITE_DATA_DIR, "tools.json"),
|
path.join(WEBSITE_DATA_DIR, "tools.json"),
|
||||||
JSON.stringify(toolsData, null, 2)
|
JSON.stringify(toolsData, null, 2)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "accessibility-kanban",
|
||||||
|
"name": "Accessibility Kanban",
|
||||||
|
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"accessibility",
|
||||||
|
"github-issues",
|
||||||
|
"issue-triage",
|
||||||
|
"kanban-board",
|
||||||
|
"planning-workflow",
|
||||||
|
"status-tracking"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,14 @@
|
|||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "latest"
|
"@github/copilot-sdk": "latest"
|
||||||
}
|
},
|
||||||
|
"description": "Users drag accessibility issues across kanban lanes to plan, track, and complete remediation work.",
|
||||||
|
"keywords": [
|
||||||
|
"accessibility",
|
||||||
|
"kanban-board",
|
||||||
|
"issue-triage",
|
||||||
|
"planning-workflow",
|
||||||
|
"status-tracking",
|
||||||
|
"github-issues"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "backlog-swipe-triage",
|
||||||
|
"name": "Backlog Swipe Triage",
|
||||||
|
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"agent-assignment",
|
||||||
|
"backlog-triage",
|
||||||
|
"github-issues",
|
||||||
|
"issue-prioritization",
|
||||||
|
"swipe-interface",
|
||||||
|
"workflow-automation"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,16 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"description": "Swipe-driven backlog triage canvas for reviewing open issues and assigning implementation work.",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "1.0.1"
|
"@github/copilot-sdk": "1.0.1"
|
||||||
}
|
},
|
||||||
|
"description": "Users quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||||
|
"keywords": [
|
||||||
|
"backlog-triage",
|
||||||
|
"swipe-interface",
|
||||||
|
"issue-prioritization",
|
||||||
|
"github-issues",
|
||||||
|
"agent-assignment",
|
||||||
|
"workflow-automation"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"id": "chromium-control-canvas",
|
||||||
|
"name": "Chromium Control Canvas",
|
||||||
|
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"browser-control",
|
||||||
|
"chromium-browser",
|
||||||
|
"interactive-canvas",
|
||||||
|
"playwright-automation",
|
||||||
|
"screenshots",
|
||||||
|
"ui-testing",
|
||||||
|
"web-navigation"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -561,7 +561,7 @@ const session = await joinSession({
|
|||||||
id: "chromium-control-canvas",
|
id: "chromium-control-canvas",
|
||||||
displayName: "Chromium Control Canvas",
|
displayName: "Chromium Control Canvas",
|
||||||
description:
|
description:
|
||||||
"Control canvas for a real headful Chromium window driven by Playwright.",
|
"Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "chromium-control-canvas",
|
"name": "chromium-control-canvas",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "GitHub Copilot canvas that drives a real headful Chromium window via Playwright.",
|
|
||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"keywords": [],
|
|
||||||
"author": "Andrea Griffiths",
|
"author": "Andrea Griffiths",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "latest",
|
"@github/copilot-sdk": "latest",
|
||||||
"playwright": "^1.60.0"
|
"playwright": "^1.60.0"
|
||||||
}
|
},
|
||||||
|
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||||
|
"keywords": [
|
||||||
|
"chromium-browser",
|
||||||
|
"playwright-automation",
|
||||||
|
"browser-control",
|
||||||
|
"interactive-canvas",
|
||||||
|
"web-navigation",
|
||||||
|
"screenshots",
|
||||||
|
"ui-testing"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "color-orb",
|
||||||
|
"name": "Color Orb",
|
||||||
|
"description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"agent-actions",
|
||||||
|
"color-picker",
|
||||||
|
"interactive-demo",
|
||||||
|
"realtime-updates",
|
||||||
|
"sse-events",
|
||||||
|
"visual-feedback"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,14 @@
|
|||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "latest"
|
"@github/copilot-sdk": "latest"
|
||||||
}
|
},
|
||||||
|
"description": "Gives users a visual orb they can ask the agent to recolor while showing a live activity log in the canvas.",
|
||||||
|
"keywords": [
|
||||||
|
"color-picker",
|
||||||
|
"interactive-demo",
|
||||||
|
"agent-actions",
|
||||||
|
"realtime-updates",
|
||||||
|
"sse-events",
|
||||||
|
"visual-feedback"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "diagram",
|
||||||
|
"name": "Diagram Explorer",
|
||||||
|
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"architecture-mapping",
|
||||||
|
"canvas-navigation",
|
||||||
|
"exploratory-analysis",
|
||||||
|
"interactive-diagrams",
|
||||||
|
"node-drilldown",
|
||||||
|
"relationship-visualization"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,14 @@
|
|||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "latest"
|
"@github/copilot-sdk": "latest"
|
||||||
}
|
},
|
||||||
|
"description": "Lets users render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||||
|
"keywords": [
|
||||||
|
"interactive-diagrams",
|
||||||
|
"architecture-mapping",
|
||||||
|
"node-drilldown",
|
||||||
|
"relationship-visualization",
|
||||||
|
"exploratory-analysis",
|
||||||
|
"canvas-navigation"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,21 @@
|
|||||||
"id": "coffilot",
|
"id": "coffilot",
|
||||||
"name": "Coffilot",
|
"name": "Coffilot",
|
||||||
"description": "Java-focused Copilot canvas extension from jdubois.",
|
"description": "Java-focused Copilot canvas extension from jdubois.",
|
||||||
|
"keywords": [
|
||||||
|
"java",
|
||||||
|
"canvas",
|
||||||
|
"productivity"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "extensions/external-assets/coffilot-preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "extensions/external-assets/coffilot-preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
},
|
||||||
"installUrl": "https://github.com/jdubois/coffilot",
|
"installUrl": "https://github.com/jdubois/coffilot",
|
||||||
"sourceUrl": "https://github.com/jdubois/coffilot",
|
"sourceUrl": "https://github.com/jdubois/coffilot",
|
||||||
"imagePath": "extensions/external-assets/coffilot-preview.png"
|
"imagePath": "extensions/external-assets/coffilot-preview.png"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "feedback-themes",
|
||||||
|
"name": "Feedback Themes",
|
||||||
|
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"customer-feedback",
|
||||||
|
"impact-prioritization",
|
||||||
|
"product-insights",
|
||||||
|
"signal-grouping",
|
||||||
|
"theme-analysis",
|
||||||
|
"trend-discovery"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,14 @@
|
|||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "latest"
|
"@github/copilot-sdk": "latest"
|
||||||
}
|
},
|
||||||
|
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||||
|
"keywords": [
|
||||||
|
"customer-feedback",
|
||||||
|
"theme-analysis",
|
||||||
|
"signal-grouping",
|
||||||
|
"impact-prioritization",
|
||||||
|
"product-insights",
|
||||||
|
"trend-discovery"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "gesture-review",
|
||||||
|
"name": "Gesture PR Review",
|
||||||
|
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"camera-input",
|
||||||
|
"gesture-control",
|
||||||
|
"github-prs",
|
||||||
|
"hands-free",
|
||||||
|
"mediapipe",
|
||||||
|
"pull-request-review"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -179,7 +179,7 @@ const canvas = createCanvas({
|
|||||||
id: "gesture-review",
|
id: "gesture-review",
|
||||||
displayName: "Gesture PR Review",
|
displayName: "Gesture PR Review",
|
||||||
description:
|
description:
|
||||||
"Interactive PR review using hand gestures. Shows a live camera feed and detects thumbs up (approve) or thumbs down (reject) via MediaPipe hand tracking.",
|
"Users review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: "show_pr",
|
name: "show_pr",
|
||||||
|
|||||||
@@ -5,5 +5,14 @@
|
|||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "latest"
|
"@github/copilot-sdk": "latest"
|
||||||
}
|
},
|
||||||
|
"description": "Users review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||||
|
"keywords": [
|
||||||
|
"pull-request-review",
|
||||||
|
"gesture-control",
|
||||||
|
"camera-input",
|
||||||
|
"hands-free",
|
||||||
|
"github-prs",
|
||||||
|
"mediapipe"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "release-notes-showcase",
|
||||||
|
"name": "Release Notes Showcase",
|
||||||
|
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"changelog",
|
||||||
|
"contributor-callouts",
|
||||||
|
"email-export",
|
||||||
|
"launch-summary",
|
||||||
|
"product-updates",
|
||||||
|
"release-notes"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,16 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "extension.mjs",
|
"main": "extension.mjs",
|
||||||
"description": "Release notes canvas for building polished release communications and exports.",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/copilot-sdk": "1.0.1"
|
"@github/copilot-sdk": "1.0.1"
|
||||||
}
|
},
|
||||||
|
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||||
|
"keywords": [
|
||||||
|
"release-notes",
|
||||||
|
"launch-summary",
|
||||||
|
"changelog",
|
||||||
|
"contributor-callouts",
|
||||||
|
"product-updates",
|
||||||
|
"email-export"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -621,7 +621,7 @@ export const releaseNotesShowcaseCanvas = createCanvas({
|
|||||||
id: CANVAS_ID,
|
id: CANVAS_ID,
|
||||||
displayName: CANVAS_TITLE,
|
displayName: CANVAS_TITLE,
|
||||||
description:
|
description:
|
||||||
"Presents release notes as a high-impact launch summary with contributor callouts and email-ready export output.",
|
"Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||||
inputSchema: releaseNotesInputSchema,
|
inputSchema: releaseNotesInputSchema,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "where-was-i",
|
||||||
|
"name": "Where Was I?",
|
||||||
|
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"branch-state",
|
||||||
|
"developer-context",
|
||||||
|
"git-history",
|
||||||
|
"interrupt-recovery",
|
||||||
|
"pull-request-context",
|
||||||
|
"resume-work"
|
||||||
|
],
|
||||||
|
"screenshots": {
|
||||||
|
"icon": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"path": "assets/preview.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -666,7 +666,7 @@ const session = await joinSession({
|
|||||||
createCanvas({
|
createCanvas({
|
||||||
id: "where-was-i",
|
id: "where-was-i",
|
||||||
displayName: "Where Was I?",
|
displayName: "Where Was I?",
|
||||||
description: "Interrupt Recovery — reconstructs your working context (branch, commits, changes, PRs) so you can resume after being pulled away.",
|
description: "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: "refresh",
|
name: "refresh",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "where-was-i",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "extension.mjs",
|
||||||
|
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||||
|
"keywords": [
|
||||||
|
"interrupt-recovery",
|
||||||
|
"developer-context",
|
||||||
|
"git-history",
|
||||||
|
"branch-state",
|
||||||
|
"resume-work",
|
||||||
|
"pull-request-context"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@github/copilot-sdk": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,13 @@ const initialItems = sortExtensions(extensionsData.items, 'title');
|
|||||||
<div class="listing-toolbar-row">
|
<div class="listing-toolbar-row">
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} extensions</div>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} extensions</div>
|
||||||
<details class="listing-controls">
|
<details class="listing-controls">
|
||||||
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort</summary>
|
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort & Filter</summary>
|
||||||
<div class="listing-controls-panel">
|
<div class="listing-controls-panel">
|
||||||
<div class="filters-bar" id="filters-bar">
|
<div class="filters-bar" id="filters-bar">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="filter-keyword">Keyword:</label>
|
||||||
|
<select id="filter-keyword" multiple aria-label="Filter by keyword"></select>
|
||||||
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="sort-select">Sort:</label>
|
<label for="sort-select">Sort:</label>
|
||||||
<select id="sort-select" aria-label="Sort extensions">
|
<select id="sort-select" aria-label="Sort extensions">
|
||||||
@@ -30,6 +34,7 @@ const initialItems = sortExtensions(extensionsData.items, 'title');
|
|||||||
<option value="lastUpdated">Recently Updated</option>
|
<option value="lastUpdated">Recently Updated</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -2,11 +2,26 @@ import { escapeHtml, getGitHubUrl, getLastUpdatedHtml } from "../utils";
|
|||||||
|
|
||||||
export interface RenderableExtension {
|
export interface RenderableExtension {
|
||||||
id: string;
|
id: string;
|
||||||
|
canvasId?: string;
|
||||||
|
extensionId?: string;
|
||||||
|
extensionName?: string;
|
||||||
name: string;
|
name: string;
|
||||||
path?: string | null;
|
path?: string | null;
|
||||||
ref?: string | null;
|
ref?: string | null;
|
||||||
|
version?: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
lastUpdated?: string | null;
|
lastUpdated?: string | null;
|
||||||
|
keywords?: string[];
|
||||||
|
screenshots?: {
|
||||||
|
icon?: {
|
||||||
|
path?: string | null;
|
||||||
|
type?: string | null;
|
||||||
|
} | null;
|
||||||
|
gallery?: {
|
||||||
|
path?: string | null;
|
||||||
|
type?: string | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
assetPath?: string | null;
|
assetPath?: string | null;
|
||||||
installUrl?: string | null;
|
installUrl?: string | null;
|
||||||
@@ -69,6 +84,18 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
|||||||
<div class="resource-description">${escapeHtml(
|
<div class="resource-description">${escapeHtml(
|
||||||
item.description || "Canvas extension"
|
item.description || "Canvas extension"
|
||||||
)}</div>
|
)}</div>
|
||||||
|
<div class="resource-keywords">
|
||||||
|
${
|
||||||
|
item.keywords && item.keywords.length > 0
|
||||||
|
? item.keywords
|
||||||
|
.map(
|
||||||
|
(kw) =>
|
||||||
|
`<span class="keyword-tag">${escapeHtml(kw)}</span>`
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div class="resource-meta">
|
<div class="resource-meta">
|
||||||
${
|
${
|
||||||
item.external
|
item.external
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* Canvas extensions page functionality
|
* Canvas extensions page functionality
|
||||||
*/
|
*/
|
||||||
|
import {
|
||||||
|
createChoices,
|
||||||
|
getChoicesValues,
|
||||||
|
setChoicesValues,
|
||||||
|
type Choices,
|
||||||
|
} from "../choices";
|
||||||
import {
|
import {
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
fetchData,
|
fetchData,
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
|
getQueryParamValues,
|
||||||
showToast,
|
showToast,
|
||||||
updateQueryParams,
|
updateQueryParams,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
@@ -17,14 +24,22 @@ import {
|
|||||||
|
|
||||||
interface Extension extends RenderableExtension {
|
interface Extension extends RenderableExtension {
|
||||||
lastUpdated?: string | null;
|
lastUpdated?: string | null;
|
||||||
|
keywords?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtensionsData {
|
interface ExtensionsData {
|
||||||
items: Extension[];
|
items: Extension[];
|
||||||
|
filters?: {
|
||||||
|
keywords?: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let allItems: Extension[] = [];
|
let allItems: Extension[] = [];
|
||||||
let currentSort: ExtensionSortOption = "title";
|
let currentSort: ExtensionSortOption = "title";
|
||||||
|
let keywordSelect: Choices;
|
||||||
|
let currentFilters = {
|
||||||
|
keywords: [] as string[],
|
||||||
|
};
|
||||||
let actionHandlersReady = false;
|
let actionHandlersReady = false;
|
||||||
|
|
||||||
function openPreviewModal(url: string, alt: string): void {
|
function openPreviewModal(url: string, alt: string): void {
|
||||||
@@ -51,13 +66,33 @@ function closePreviewModal(): void {
|
|||||||
document.body.style.overflow = "";
|
document.body.style.overflow = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortItems(items: Extension[]): Extension[] {
|
||||||
|
return sortExtensions(items, currentSort);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCountText(resultsCount: number): string {
|
||||||
|
if (currentFilters.keywords.length === 0) {
|
||||||
|
return `${resultsCount} extension${resultsCount === 1 ? "" : "s"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${resultsCount} of ${allItems.length} extensions (filtered by ${currentFilters.keywords.length} keyword${currentFilters.keywords.length === 1 ? "" : "s"})`;
|
||||||
|
}
|
||||||
|
|
||||||
function applySortAndRender(): void {
|
function applySortAndRender(): void {
|
||||||
const countEl = document.getElementById("results-count");
|
const countEl = document.getElementById("results-count");
|
||||||
const results = sortExtensions(allItems, currentSort);
|
let results = [...allItems];
|
||||||
|
|
||||||
|
if (currentFilters.keywords.length > 0) {
|
||||||
|
results = results.filter((item) =>
|
||||||
|
item.keywords?.some((keyword) => currentFilters.keywords.includes(keyword))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
results = sortItems(results);
|
||||||
|
|
||||||
renderItems(results);
|
renderItems(results);
|
||||||
if (countEl) {
|
if (countEl) {
|
||||||
countEl.textContent = `${results.length} extension${results.length === 1 ? "" : "s"}`;
|
countEl.textContent = getCountText(results.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,12 +167,15 @@ function setupActionHandlers(list: HTMLElement | null): void {
|
|||||||
|
|
||||||
function syncUrlState(): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
|
q: "",
|
||||||
|
keyword: currentFilters.keywords,
|
||||||
sort: currentSort === "title" ? "" : currentSort,
|
sort: currentSort === "title" ? "" : currentSort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initExtensionsPage(): Promise<void> {
|
export async function initExtensionsPage(): Promise<void> {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
|
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||||
const sortSelect = document.getElementById(
|
const sortSelect = document.getElementById(
|
||||||
"sort-select"
|
"sort-select"
|
||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
@@ -154,19 +192,63 @@ export async function initExtensionsPage(): Promise<void> {
|
|||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
|
|
||||||
|
const availableKeywords = (
|
||||||
|
data.filters?.keywords ||
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
data.items.flatMap((item) =>
|
||||||
|
Array.isArray(item.keywords) ? item.keywords : []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
keywordSelect = createChoices("#filter-keyword", {
|
||||||
|
placeholderValue: "All Keywords",
|
||||||
|
});
|
||||||
|
keywordSelect.setChoices(
|
||||||
|
availableKeywords.map((keyword) => ({ value: keyword, label: keyword })),
|
||||||
|
"value",
|
||||||
|
"label",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialKeywords = getQueryParamValues("keyword").filter((keyword) =>
|
||||||
|
availableKeywords.includes(keyword)
|
||||||
|
);
|
||||||
const initialSort = getQueryParam("sort");
|
const initialSort = getQueryParam("sort");
|
||||||
|
if (initialKeywords.length > 0) {
|
||||||
|
currentFilters.keywords = initialKeywords;
|
||||||
|
setChoicesValues(keywordSelect, initialKeywords);
|
||||||
|
}
|
||||||
if (initialSort === "lastUpdated") {
|
if (initialSort === "lastUpdated") {
|
||||||
currentSort = initialSort;
|
currentSort = initialSort;
|
||||||
if (sortSelect) sortSelect.value = initialSort;
|
if (sortSelect) sortSelect.value = initialSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById("filter-keyword")?.addEventListener("change", () => {
|
||||||
|
currentFilters.keywords = getChoicesValues(keywordSelect);
|
||||||
|
applySortAndRender();
|
||||||
|
syncUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
sortSelect?.addEventListener("change", () => {
|
sortSelect?.addEventListener("change", () => {
|
||||||
currentSort = sortSelect.value as ExtensionSortOption;
|
currentSort = sortSelect.value as ExtensionSortOption;
|
||||||
applySortAndRender();
|
applySortAndRender();
|
||||||
syncUrlState();
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearFiltersBtn?.addEventListener("click", () => {
|
||||||
|
currentFilters = { keywords: [] };
|
||||||
|
currentSort = "title";
|
||||||
|
keywordSelect.removeActiveItems();
|
||||||
|
if (sortSelect) sortSelect.value = "title";
|
||||||
|
applySortAndRender();
|
||||||
|
syncUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
applySortAndRender();
|
applySortAndRender();
|
||||||
|
syncUrlState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-initialize when DOM is ready
|
// Auto-initialize when DOM is ready
|
||||||
|
|||||||
@@ -1971,6 +1971,23 @@ body:has(#main-content) {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-keywords {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
.resource-actions {
|
.resource-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user