diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs index f20a8bbe..875eb803 100755 --- a/eng/generate-website-data.mjs +++ b/eng/generate-website-data.mjs @@ -101,6 +101,25 @@ function normalizeText(value, fallback = "") { return typeof value === "string" ? value.trim() : fallback; } +/** + * Normalize an author value (npm string form or { name, url } object) to + * { name, url? } | null. Returns null when no usable name is present. + */ +function normalizeAuthor(value) { + if (!value) return null; + if (typeof value === "string") { + const name = value.trim(); + return name ? { name } : null; + } + if (typeof value === "object") { + const name = normalizeText(value.name); + if (!name) return null; + const url = normalizeText(value.url); + return url ? { name, url } : { name }; + } + return null; +} + /** * Find the latest git-modified date for any file under a directory. */ @@ -1002,6 +1021,10 @@ function generateCanvasManifest(gitDates, commitSha) { const packageJson = fs.existsSync(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}; + const canvasJsonPath = path.join(extensionDir, "canvas.json"); + const canvasJson = fs.existsSync(canvasJsonPath) + ? JSON.parse(fs.readFileSync(canvasJsonPath, "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)) : []; @@ -1044,6 +1067,7 @@ function generateCanvasManifest(gitDates, commitSha) { installUrl, sourceUrl: null, external: false, + author: normalizeAuthor(canvasJson.author), keywords, }); } @@ -1116,6 +1140,7 @@ function generateCanvasManifest(gitDates, commitSha) { installUrl, sourceUrl: sourceUrl || null, external: true, + author: normalizeAuthor(ext?.author), keywords, }); } @@ -1199,6 +1224,7 @@ function writePerExtensionCanvasManifests(canvasManifestData) { name: item.name, description: item.description || "Canvas extension", version: item.version || "1.0.0", + ...(item.author ? { author: item.author } : {}), keywords: Array.isArray(item.keywords) ? [...new Set(item.keywords)].sort((a, b) => a.localeCompare(b)) : [], diff --git a/extensions/accessibility-kanban/canvas.json b/extensions/accessibility-kanban/canvas.json index 6bc4538e..c9aecb27 100644 --- a/extensions/accessibility-kanban/canvas.json +++ b/extensions/accessibility-kanban/canvas.json @@ -3,6 +3,10 @@ "name": "Accessibility Kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", "version": "1.0.0", + "author": { + "name": "Aaron Powell", + "url": "https://github.com/aaronpowell" + }, "keywords": [ "accessibility", "github-issues", diff --git a/extensions/arcade-canvas/canvas.json b/extensions/arcade-canvas/canvas.json index 259a1264..daf543c6 100644 --- a/extensions/arcade-canvas/canvas.json +++ b/extensions/arcade-canvas/canvas.json @@ -3,6 +3,10 @@ "name": "Agent Arcade", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", "version": "1.0.0", + "author": { + "name": "Dan Wahlin", + "url": "https://github.com/DanWahlin" + }, "keywords": [ "arcade-games", "copilot-canvas", diff --git a/extensions/backlog-swipe-triage/canvas.json b/extensions/backlog-swipe-triage/canvas.json index b4b5b350..08dd7ead 100644 --- a/extensions/backlog-swipe-triage/canvas.json +++ b/extensions/backlog-swipe-triage/canvas.json @@ -3,6 +3,10 @@ "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", + "author": { + "name": "James Montemagno", + "url": "https://github.com/jamesmontemagno" + }, "keywords": [ "agent-assignment", "backlog-triage", diff --git a/extensions/chromium-control-canvas/canvas.json b/extensions/chromium-control-canvas/canvas.json index 79ff4cf6..3325ca63 100644 --- a/extensions/chromium-control-canvas/canvas.json +++ b/extensions/chromium-control-canvas/canvas.json @@ -3,6 +3,10 @@ "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", + "author": { + "name": "Andrea Griffiths", + "url": "https://github.com/AndreaGriffiths11" + }, "keywords": [ "browser-control", "chromium-browser", diff --git a/extensions/color-orb/canvas.json b/extensions/color-orb/canvas.json index 47a1a32b..a68404ed 100644 --- a/extensions/color-orb/canvas.json +++ b/extensions/color-orb/canvas.json @@ -3,6 +3,10 @@ "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", + "author": { + "name": "Aaron Powell", + "url": "https://github.com/aaronpowell" + }, "keywords": [ "agent-actions", "color-picker", diff --git a/extensions/diagram-viewer/canvas.json b/extensions/diagram-viewer/canvas.json index 0fe72fde..ddc3b4d2 100644 --- a/extensions/diagram-viewer/canvas.json +++ b/extensions/diagram-viewer/canvas.json @@ -3,6 +3,10 @@ "name": "Diagram Explorer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", "version": "1.0.0", + "author": { + "name": "Aaron Powell", + "url": "https://github.com/aaronpowell" + }, "keywords": [ "architecture-mapping", "canvas-navigation", diff --git a/extensions/external.json b/extensions/external.json index c6b98bcc..f0e2f561 100644 --- a/extensions/external.json +++ b/extensions/external.json @@ -3,6 +3,7 @@ "id": "coffilot", "name": "Coffilot", "description": "Java-focused Copilot canvas extension from jdubois.", + "author": { "name": "Julien Dubois", "url": "https://github.com/jdubois" }, "keywords": [ "java", "canvas", diff --git a/extensions/feedback-themes/canvas.json b/extensions/feedback-themes/canvas.json index 01a269be..3bc88213 100644 --- a/extensions/feedback-themes/canvas.json +++ b/extensions/feedback-themes/canvas.json @@ -3,6 +3,10 @@ "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", + "author": { + "name": "Aaron Powell", + "url": "https://github.com/aaronpowell" + }, "keywords": [ "customer-feedback", "impact-prioritization", diff --git a/extensions/gesture-review/canvas.json b/extensions/gesture-review/canvas.json index c05ecddf..61f91cdc 100644 --- a/extensions/gesture-review/canvas.json +++ b/extensions/gesture-review/canvas.json @@ -3,6 +3,10 @@ "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", + "author": { + "name": "Aaron Powell", + "url": "https://github.com/aaronpowell" + }, "keywords": [ "camera-input", "gesture-control", diff --git a/extensions/release-notes-showcase/canvas.json b/extensions/release-notes-showcase/canvas.json index a21dbbb2..4b1b1835 100644 --- a/extensions/release-notes-showcase/canvas.json +++ b/extensions/release-notes-showcase/canvas.json @@ -3,6 +3,10 @@ "name": "Release Notes Showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", "version": "1.0.0", + "author": { + "name": "James Montemagno", + "url": "https://github.com/jamesmontemagno" + }, "keywords": [ "changelog", "contributor-callouts", diff --git a/extensions/where-was-i/canvas.json b/extensions/where-was-i/canvas.json index 2909dfb2..bf5a69e3 100644 --- a/extensions/where-was-i/canvas.json +++ b/extensions/where-was-i/canvas.json @@ -3,6 +3,10 @@ "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", + "author": { + "name": "Aaron Powell", + "url": "https://github.com/aaronpowell" + }, "keywords": [ "branch-state", "developer-context", diff --git a/website/src/scripts/pages/extensions-render.ts b/website/src/scripts/pages/extensions-render.ts index ac8f2539..8172cef5 100644 --- a/website/src/scripts/pages/extensions-render.ts +++ b/website/src/scripts/pages/extensions-render.ts @@ -1,4 +1,10 @@ -import { escapeHtml, getGitHubUrl, getLastUpdatedHtml } from "../utils"; +import { + escapeHtml, + getGitHubHandle, + getGitHubUrl, + getLastUpdatedHtml, + sanitizeUrl, +} from "../utils"; import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render"; export interface RenderableExtension { @@ -34,6 +40,7 @@ export interface RenderableExtension { installUrl?: string | null; sourceUrl?: string | null; external?: boolean; + author?: { name: string; url?: string } | null; } export type ExtensionSortOption = "title" | "lastUpdated"; @@ -92,8 +99,27 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string { `; + const authorName = item.author?.name; + const authorUrl = item.author?.url; + const authorHandle = + authorName && authorUrl + ? getGitHubHandle(authorUrl, authorName) + : authorName || ""; + const authorHtml = authorName + ? `by ${ + authorUrl + ? `${escapeHtml(authorHandle)}` + : escapeHtml(authorName) + }` + : ""; + const metaHtml = ` ${item.external ? 'External' : ""} + ${authorHtml} ${getLastUpdatedHtml(item.lastUpdated)} `; diff --git a/website/src/scripts/pages/extensions.ts b/website/src/scripts/pages/extensions.ts index 3c3c9c94..09edf1de 100644 --- a/website/src/scripts/pages/extensions.ts +++ b/website/src/scripts/pages/extensions.ts @@ -12,9 +12,11 @@ import { copyToClipboard, fetchData, formatRelativeTime, + getGitHubHandle, getGitHubUrl, getQueryParam, getQueryParamValues, + sanitizeUrl, showToast, updateQueryParams, } from "../utils"; @@ -178,6 +180,24 @@ function openDetailsModal( if (item.external) { metaParts.push('External'); } + if (item.author?.name) { + const authorName = item.author.name; + const authorUrl = item.author.url; + const authorHandle = authorUrl + ? getGitHubHandle(authorUrl, authorName) + : authorName; + metaParts.push( + authorUrl + ? `by ${escapeHtml(authorHandle)}` + : `by ${escapeHtml( + authorName + )}` + ); + } if (item.lastUpdated) { metaParts.push( `Updated ${escapeHtml( diff --git a/website/src/scripts/utils.ts b/website/src/scripts/utils.ts index 30cc1fac..b90d7793 100644 --- a/website/src/scripts/utils.ts +++ b/website/src/scripts/utils.ts @@ -389,6 +389,31 @@ export function sanitizeUrl(url: string | null | undefined): string { return "#"; } +/** + * Derive a GitHub @handle from a profile URL + * (e.g. "https://github.com/aaronpowell" -> "@aaronpowell"). + * Falls back to the provided value when the URL is not a github.com profile. + */ +export function getGitHubHandle( + url: string | null | undefined, + fallback = "" +): string { + if (!url) return fallback; + try { + const parsed = new URL(url); + const host = parsed.hostname.replace(/^www\./, ""); + if (host === "github.com") { + const segments = parsed.pathname.split("/").filter(Boolean); + if (segments.length === 1) { + return `@${segments[0]}`; + } + } + } catch { + // Invalid URL + } + return fallback; +} + /** * Truncate text with ellipsis */