From f5ac9768365f2665a9b846ed39b4a378041a39d6 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 25 Feb 2026 16:03:02 +1100 Subject: [PATCH] Add agentic workflows page to website Add a new /workflows/ page for browsing agentic workflow definitions with search, trigger/tag filters, and sorting. Follows the same patterns as the existing hooks page. New files: - website/src/pages/workflows.astro - website/src/scripts/pages/workflows.ts Updated files: - BaseLayout.astro: add Workflows nav link - index.astro: add Workflows card to homepage - pages/index.ts: add workflows to counts - utils.ts: add workflow type to icons, labels, and getResourceType Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/src/layouts/BaseLayout.astro | 4 + website/src/pages/index.astro | 8 + website/src/pages/workflows.astro | 54 ++++++ website/src/scripts/pages/index.ts | 3 +- website/src/scripts/pages/workflows.ts | 235 +++++++++++++++++++++++++ website/src/scripts/utils.ts | 4 + 6 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 website/src/pages/workflows.astro create mode 100644 website/src/scripts/pages/workflows.ts diff --git a/website/src/layouts/BaseLayout.astro b/website/src/layouts/BaseLayout.astro index 6f114e9c..5efa9cae 100644 --- a/website/src/layouts/BaseLayout.astro +++ b/website/src/layouts/BaseLayout.astro @@ -70,6 +70,10 @@ try { href={`${base}hooks/`} class:list={[{ active: activeNav === "hooks" }]}>Hooks + Workflows
-
+ + +
+

Workflows

+

AI-powered automations for GitHub Actions

+
+
-
+
diff --git a/website/src/pages/workflows.astro b/website/src/pages/workflows.astro new file mode 100644 index 00000000..5f4074d2 --- /dev/null +++ b/website/src/pages/workflows.astro @@ -0,0 +1,54 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import Modal from '../components/Modal.astro'; +--- + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
Loading workflows...
+
+
+
+
+ + + + +
diff --git a/website/src/scripts/pages/index.ts b/website/src/scripts/pages/index.ts index 82fbcd12..54dac8de 100644 --- a/website/src/scripts/pages/index.ts +++ b/website/src/scripts/pages/index.ts @@ -11,6 +11,7 @@ interface Manifest { instructions: number; skills: number; hooks: number; + workflows: number; plugins: number; tools: number; }; @@ -35,7 +36,7 @@ export async function initHomepage(): Promise { const manifest = await fetchData('manifest.json'); if (manifest && manifest.counts) { // Populate counts in cards - const countKeys = ['agents', 'instructions', 'skills', 'hooks', 'plugins', 'tools'] as const; + const countKeys = ['agents', 'instructions', 'skills', 'hooks', 'workflows', 'plugins', 'tools'] as const; countKeys.forEach(key => { const countEl = document.querySelector(`.card-count[data-count="${key}"]`); if (countEl && manifest.counts[key] !== undefined) { diff --git a/website/src/scripts/pages/workflows.ts b/website/src/scripts/pages/workflows.ts new file mode 100644 index 00000000..ea76d569 --- /dev/null +++ b/website/src/scripts/pages/workflows.ts @@ -0,0 +1,235 @@ +/** + * Workflows page functionality + */ +import { createChoices, getChoicesValues, type Choices } from "../choices"; +import { FuzzySearch, type SearchItem } from "../search"; +import { + fetchData, + debounce, + escapeHtml, + getGitHubUrl, + getActionButtonsHtml, + setupActionHandlers, + getLastUpdatedHtml, +} from "../utils"; +import { setupModal, openFileModal } from "../modal"; + +interface Workflow extends SearchItem { + id: string; + path: string; + triggers: string[]; + tags: string[]; + lastUpdated?: string | null; +} + +interface WorkflowsData { + items: Workflow[]; + filters: { + triggers: string[]; + tags: string[]; + }; +} + +type SortOption = "title" | "lastUpdated"; + +const resourceType = "workflow"; +let allItems: Workflow[] = []; +let search = new FuzzySearch(); +let triggerSelect: Choices; +let tagSelect: Choices; +let currentFilters = { + triggers: [] as string[], + tags: [] as string[], +}; +let currentSort: SortOption = "title"; + +function sortItems(items: Workflow[]): Workflow[] { + return [...items].sort((a, b) => { + if (currentSort === "lastUpdated") { + const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0; + const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0; + return dateB - dateA; + } + return a.title.localeCompare(b.title); + }); +} + +function applyFiltersAndRender(): void { + const searchInput = document.getElementById( + "search-input" + ) as HTMLInputElement; + const countEl = document.getElementById("results-count"); + const query = searchInput?.value || ""; + + let results = query ? search.search(query) : [...allItems]; + + if (currentFilters.triggers.length > 0) { + results = results.filter((item) => + item.triggers.some((t) => currentFilters.triggers.includes(t)) + ); + } + if (currentFilters.tags.length > 0) { + results = results.filter((item) => + item.tags.some((t) => currentFilters.tags.includes(t)) + ); + } + + results = sortItems(results); + + renderItems(results, query); + const activeFilters: string[] = []; + if (currentFilters.triggers.length > 0) + activeFilters.push( + `${currentFilters.triggers.length} trigger${ + currentFilters.triggers.length > 1 ? "s" : "" + }` + ); + if (currentFilters.tags.length > 0) + activeFilters.push( + `${currentFilters.tags.length} tag${ + currentFilters.tags.length > 1 ? "s" : "" + }` + ); + let countText = `${results.length} of ${allItems.length} workflows`; + if (activeFilters.length > 0) { + countText += ` (filtered by ${activeFilters.join(", ")})`; + } + if (countEl) countEl.textContent = countText; +} + +function renderItems(items: Workflow[], query = ""): void { + const list = document.getElementById("resource-list"); + if (!list) return; + + if (items.length === 0) { + list.innerHTML = + '

No workflows found

Try a different search term or adjust filters

'; + return; + } + + list.innerHTML = items + .map( + (item) => ` +
+ ` + ) + .join(""); + + // Add click handlers for opening modal + list.querySelectorAll(".resource-item").forEach((el) => { + el.addEventListener("click", (e) => { + if ((e.target as HTMLElement).closest(".resource-actions")) return; + const path = (el as HTMLElement).dataset.path; + if (path) openFileModal(path, resourceType); + }); + }); +} + +export async function initWorkflowsPage(): Promise { + const list = document.getElementById("resource-list"); + const searchInput = document.getElementById( + "search-input" + ) as HTMLInputElement; + const clearFiltersBtn = document.getElementById("clear-filters"); + const sortSelect = document.getElementById( + "sort-select" + ) as HTMLSelectElement; + + const data = await fetchData("workflows.json"); + if (!data || !data.items) { + if (list) + list.innerHTML = + '

Failed to load data

'; + return; + } + + allItems = data.items; + search.setItems(allItems); + + // Setup trigger filter + triggerSelect = createChoices("#filter-trigger", { + placeholderValue: "All Triggers", + }); + triggerSelect.setChoices( + data.filters.triggers.map((t) => ({ value: t, label: t })), + "value", + "label", + true + ); + document.getElementById("filter-trigger")?.addEventListener("change", () => { + currentFilters.triggers = getChoicesValues(triggerSelect); + applyFiltersAndRender(); + }); + + // Setup tag filter + tagSelect = createChoices("#filter-tag", { + placeholderValue: "All Tags", + }); + tagSelect.setChoices( + data.filters.tags.map((t) => ({ value: t, label: t })), + "value", + "label", + true + ); + document.getElementById("filter-tag")?.addEventListener("change", () => { + currentFilters.tags = getChoicesValues(tagSelect); + applyFiltersAndRender(); + }); + + sortSelect?.addEventListener("change", () => { + currentSort = sortSelect.value as SortOption; + applyFiltersAndRender(); + }); + + applyFiltersAndRender(); + searchInput?.addEventListener( + "input", + debounce(() => applyFiltersAndRender(), 200) + ); + + clearFiltersBtn?.addEventListener("click", () => { + currentFilters = { triggers: [], tags: [] }; + currentSort = "title"; + triggerSelect.removeActiveItems(); + tagSelect.removeActiveItems(); + if (searchInput) searchInput.value = ""; + if (sortSelect) sortSelect.value = "title"; + applyFiltersAndRender(); + }); + + setupModal(); + setupActionHandlers(); +} + +// Auto-initialize when DOM is ready +document.addEventListener("DOMContentLoaded", initWorkflowsPage); diff --git a/website/src/scripts/utils.ts b/website/src/scripts/utils.ts index ee6fe2a9..6ea97853 100644 --- a/website/src/scripts/utils.ts +++ b/website/src/scripts/utils.ts @@ -228,6 +228,8 @@ export function getResourceType(filePath: string): string { return "skill"; if (/(^|\/)hooks\//.test(filePath) && filePath.endsWith("README.md")) return "hook"; + if (/(^|\/)workflows\//.test(filePath) && filePath.endsWith(".md")) + return "workflow"; // Check for plugin directories (e.g., plugins/, plugins//) if (/(^|\/)plugins\/[^/]+\/?$/.test(filePath)) return "plugin"; // Check for plugin.json files (e.g., plugins//.github/plugin/plugin.json) @@ -244,6 +246,7 @@ export function formatResourceType(type: string): string { instruction: "📋 Instruction", skill: "⚡ Skill", hook: "🪝 Hook", + workflow: "⚡ Workflow", plugin: "🔌 Plugin", }; return labels[type] || type; @@ -258,6 +261,7 @@ export function getResourceIcon(type: string): string { instruction: "📋", skill: "⚡", hook: "🪝", + workflow: "⚡", plugin: "🔌", }; return icons[type] || "📄";