From 8862c6d1ac0b01dd39e21679a636d50ca7c1f6c6 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 8 Oct 2026 19:21:11 +1100 Subject: [PATCH 1/4] fix(search): offer indexing for empty repository searches --- layers/marketing/app/pages/skills/index.vue | 21 +++++++++++++++- .../app/utils/registry-view-state.ts | 11 ++++++++ test/unit/registry-view-state.test.ts | 25 ++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/layers/marketing/app/pages/skills/index.vue b/layers/marketing/app/pages/skills/index.vue index 965283dd..15554135 100644 --- a/layers/marketing/app/pages/skills/index.vue +++ b/layers/marketing/app/pages/skills/index.vue @@ -4,7 +4,7 @@ import { createReusableTemplate, useMediaQuery } from '@vueuse/core' import { githubAvatarProxyUrl } from '#shared/image-proxy' import { trendingSkillKey, trendingSkillKeySet } from '#shared/trending-keys' import DirectoryCta from '../../components/skills/_DirectoryCta.vue' -import { isInputFocused, resolveRegistryViewState } from '../../utils/registry-view-state' +import { isInputFocused, repositorySearchFallback, resolveRegistryViewState } from '../../utils/registry-view-state' // "Find skills for your AI agent" matched no query anyone types. The demand is // on `claude skills directory` (260/mo), `agent skills directory` (90), and the @@ -179,6 +179,15 @@ const totalPages = computed(() => registryView.value._tag === 'ready' ? registryView.value.data.pages : 1, ) const isLoading = computed(() => registryView.value._tag === 'loading') +const { submitRepository, repositoryTask } = useSkillSearch() +const repositoryFallback = computed(() => search.value.trim() === searchQuery.value.trim() + ? repositorySearchFallback(searchQuery.value, registryStatus.value, registryData.value?.total) + : null) + +function indexSearchRepository(): void { + if (repositoryFallback.value) + void submitRepository(repositoryFallback.value) +} const searchInput = ref<{ inputRef?: HTMLInputElement } | null>(null) const activeElement = useActiveElement() @@ -496,6 +505,16 @@ function selectOwner(next: string) {

Remove a tag or try a broader search.

+ = | { _tag: 'loading' } | { _tag: 'error', error: unknown } diff --git a/test/unit/registry-view-state.test.ts b/test/unit/registry-view-state.test.ts index 6dc05e62..a9da4849 100644 --- a/test/unit/registry-view-state.test.ts +++ b/test/unit/registry-view-state.test.ts @@ -1,5 +1,28 @@ import { describe, expect, it } from 'vitest' -import { isInputFocused, resolveRegistryViewState } from '../../layers/marketing/app/utils/registry-view-state' +import { isInputFocused, repositorySearchFallback, resolveRegistryViewState } from '../../layers/marketing/app/utils/registry-view-state' + +describe('repositorySearchFallback', () => { + it.each(['vojtaholik/good-css', 'https://github.com/vojtaholik/good-css'])('offers indexing for an empty search for %s', (query) => { + expect(repositorySearchFallback(query, 'success', 0)).toEqual({ + _tag: 'repository', + owner: 'vojtaholik', + repo: 'good-css', + url: 'https://github.com/vojtaholik/good-css', + }) + }) + + it.each(['idle', 'pending', 'error'] as const)('does not offer indexing while search is %s', (status) => { + expect(repositorySearchFallback('vojtaholik/good-css', status, 0)).toBeNull() + }) + + it('keeps existing results', () => { + expect(repositorySearchFallback('vojtaholik/good-css', 'success', 1)).toBeNull() + }) + + it.each(['css', 'build a page', '@vojtaholik', 'owner/repo/skill'])('keeps ordinary empty search for %s', (query) => { + expect(repositorySearchFallback(query, 'success', 0)).toBeNull() + }) +}) describe('resolveRegistryViewState', () => { it('keeps an idle conditional fetch in the loading state', () => { From 187943dc04c021dcd16f050822125d022dd5131d Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 8 Oct 2026 19:26:10 +1100 Subject: [PATCH 2/4] fix(search): wait for repository lookup before submission --- app/composables/useSkillSearchBox.ts | 21 +++++++++ layers/marketing/app/pages/skills/index.vue | 21 +-------- .../app/utils/registry-view-state.ts | 11 ----- test/unit/home-search.nuxt.test.ts | 46 +++++++++++++++++++ test/unit/registry-view-state.test.ts | 25 +--------- test/unit/skill-search-state.test.ts | 4 +- 6 files changed, 71 insertions(+), 57 deletions(-) diff --git a/app/composables/useSkillSearchBox.ts b/app/composables/useSkillSearchBox.ts index 46b71483..7b71e690 100644 --- a/app/composables/useSkillSearchBox.ts +++ b/app/composables/useSkillSearchBox.ts @@ -1,4 +1,5 @@ import type { SearchRow } from './useSkillSearch' +import { classifySearchQuery } from '#shared/skill-search-query' import { searchCellId, searchResultsRoute } from './useSkillSearch' /** The id the panel's grid takes, so every input points at the same element. */ @@ -35,6 +36,20 @@ export function useSkillSearchBox(options: { inputs: () => (HTMLInputElement | n } = search const keyboardSelection = ref(false) + const pendingRepositoryQuery = ref(null) + + watch([trimmedQuery, open], ([term, visible]) => { + if (!visible || term !== pendingRepositoryQuery.value) + pendingRepositoryQuery.value = null + }, { flush: 'sync' }) + + watch(state, (current) => { + if (pendingRepositoryQuery.value === null || current._tag === 'loading') + return + pendingRepositoryQuery.value = null + if (current._tag !== 'error') + submitQuery() + }) function clearSelection(): void { keyboardSelection.value = false @@ -91,6 +106,12 @@ export function useSkillSearchBox(options: { inputs: () => (HTMLInputElement | n function submitQuery(): void { const current = state.value + // Enter can beat the debounced lookup. Keep repository intent until it answers. + if (current._tag === 'loading' && classifySearchQuery(trimmedQuery.value)._tag === 'repository') { + openPanel() + pendingRepositoryQuery.value = trimmedQuery.value + return + } if (current._tag === 'repository') { if (current.status._tag === 'idle') void submitRepository(current.repository) diff --git a/layers/marketing/app/pages/skills/index.vue b/layers/marketing/app/pages/skills/index.vue index 15554135..965283dd 100644 --- a/layers/marketing/app/pages/skills/index.vue +++ b/layers/marketing/app/pages/skills/index.vue @@ -4,7 +4,7 @@ import { createReusableTemplate, useMediaQuery } from '@vueuse/core' import { githubAvatarProxyUrl } from '#shared/image-proxy' import { trendingSkillKey, trendingSkillKeySet } from '#shared/trending-keys' import DirectoryCta from '../../components/skills/_DirectoryCta.vue' -import { isInputFocused, repositorySearchFallback, resolveRegistryViewState } from '../../utils/registry-view-state' +import { isInputFocused, resolveRegistryViewState } from '../../utils/registry-view-state' // "Find skills for your AI agent" matched no query anyone types. The demand is // on `claude skills directory` (260/mo), `agent skills directory` (90), and the @@ -179,15 +179,6 @@ const totalPages = computed(() => registryView.value._tag === 'ready' ? registryView.value.data.pages : 1, ) const isLoading = computed(() => registryView.value._tag === 'loading') -const { submitRepository, repositoryTask } = useSkillSearch() -const repositoryFallback = computed(() => search.value.trim() === searchQuery.value.trim() - ? repositorySearchFallback(searchQuery.value, registryStatus.value, registryData.value?.total) - : null) - -function indexSearchRepository(): void { - if (repositoryFallback.value) - void submitRepository(repositoryFallback.value) -} const searchInput = ref<{ inputRef?: HTMLInputElement } | null>(null) const activeElement = useActiveElement() @@ -505,16 +496,6 @@ function selectOwner(next: string) {

Remove a tag or try a broader search.

- = | { _tag: 'loading' } | { _tag: 'error', error: unknown } diff --git a/test/unit/home-search.nuxt.test.ts b/test/unit/home-search.nuxt.test.ts index c3e61d19..52037109 100644 --- a/test/unit/home-search.nuxt.test.ts +++ b/test/unit/home-search.nuxt.test.ts @@ -92,6 +92,52 @@ afterEach(() => { }) describe('homepage search interactions', () => { + it.each(['vojtaholik/good-css', 'https://github.com/vojtaholik/good-css'])('waits for repository lookup before submitting %s', async (term) => { + const repository = { _tag: 'repository' as const, owner: 'vojtaholik', repo: 'good-css', url: 'https://github.com/vojtaholik/good-css' } + query.value = term + rows.value = [] + stateOverride.value = { _tag: 'loading', rows: [] } + const wrapper = await mountSearch() + await press(wrapper, 'Enter', 'Enter') + expect(navigate).not.toHaveBeenCalled() + expect(submitRepository).not.toHaveBeenCalled() + + stateOverride.value = { _tag: 'repository', repository, status: { _tag: 'idle' }, rows: [{ _tag: 'index', repository }] } + await flushPromises() + expect(submitRepository).toHaveBeenCalledExactlyOnceWith(repository) + expect(navigate).not.toHaveBeenCalled() + }) + + it('opens an indexed repository after a pending submission resolves', async () => { + query.value = 'vercel-labs/agent-skills' + stateOverride.value = { _tag: 'loading', rows: [] } + const wrapper = await mountSearch() + await press(wrapper, 'Enter') + expect(navigate).not.toHaveBeenCalled() + stateOverride.value = { _tag: 'ready', rows: [repositoryRow], total: 7, repository: repositoryRow.repository } + await flushPromises() + expect(navigate).toHaveBeenCalledExactlyOnceWith('/gh/vercel-labs/agent-skills') + }) + + it.each(['edit', 'Escape', 'error'])('cancels pending repository submission on %s', async (action) => { + query.value = 'vercel-labs/agent-skills' + stateOverride.value = { _tag: 'loading', rows: [] } + const wrapper = await mountSearch() + await press(wrapper, 'Enter') + if (action === 'edit') + await wrapper.get('input').setValue('vue') + else if (action === 'Escape') + await press(wrapper, 'Escape') + else { + stateOverride.value = { _tag: 'error', error: new Error('offline') } + await flushPromises() + } + stateOverride.value = { _tag: 'ready', rows: [repositoryRow], total: 7, repository: repositoryRow.repository } + await flushPromises() + expect(navigate).not.toHaveBeenCalled() + expect(submitRepository).not.toHaveBeenCalled() + }) + it('submits a name to the results page on Enter when nothing was selected', async () => { const wrapper = await mountSearch() await press(wrapper, 'Enter') diff --git a/test/unit/registry-view-state.test.ts b/test/unit/registry-view-state.test.ts index a9da4849..6dc05e62 100644 --- a/test/unit/registry-view-state.test.ts +++ b/test/unit/registry-view-state.test.ts @@ -1,28 +1,5 @@ import { describe, expect, it } from 'vitest' -import { isInputFocused, repositorySearchFallback, resolveRegistryViewState } from '../../layers/marketing/app/utils/registry-view-state' - -describe('repositorySearchFallback', () => { - it.each(['vojtaholik/good-css', 'https://github.com/vojtaholik/good-css'])('offers indexing for an empty search for %s', (query) => { - expect(repositorySearchFallback(query, 'success', 0)).toEqual({ - _tag: 'repository', - owner: 'vojtaholik', - repo: 'good-css', - url: 'https://github.com/vojtaholik/good-css', - }) - }) - - it.each(['idle', 'pending', 'error'] as const)('does not offer indexing while search is %s', (status) => { - expect(repositorySearchFallback('vojtaholik/good-css', status, 0)).toBeNull() - }) - - it('keeps existing results', () => { - expect(repositorySearchFallback('vojtaholik/good-css', 'success', 1)).toBeNull() - }) - - it.each(['css', 'build a page', '@vojtaholik', 'owner/repo/skill'])('keeps ordinary empty search for %s', (query) => { - expect(repositorySearchFallback(query, 'success', 0)).toBeNull() - }) -}) +import { isInputFocused, resolveRegistryViewState } from '../../layers/marketing/app/utils/registry-view-state' describe('resolveRegistryViewState', () => { it('keeps an idle conditional fetch in the loading state', () => { diff --git a/test/unit/skill-search-state.test.ts b/test/unit/skill-search-state.test.ts index e64d4a89..9138e2dd 100644 --- a/test/unit/skill-search-state.test.ts +++ b/test/unit/skill-search-state.test.ts @@ -125,7 +125,7 @@ describe('search results belong to the current query', () => { expect(search.rows.value).toMatchObject([{ _tag: 'skill', skill: { name: 'shipreel' } }]) }) - it('offers the index action for a Repository the registry does not hold', async () => { + it.each(['someone/new-skills', 'https://github.com/someone/new-skills'])('offers the index action for an unlisted repository entered as %s', async (query) => { fetchMock.mockResolvedValue({ kind: 'repository', repository: { _tag: 'not-indexed', owner: 'someone', repo: 'new-skills', url: 'https://github.com/someone/new-skills' }, @@ -135,7 +135,7 @@ describe('search results belong to the current query', () => { total: 0, mode: null, }) - await type('https://github.com/someone/new-skills') + await type(query) await debounce() expect(search.state.value).toMatchObject({ _tag: 'repository', status: { _tag: 'idle' } }) expect(search.rows.value).toEqual([{ _tag: 'index', repository: { _tag: 'repository', owner: 'someone', repo: 'new-skills', url: 'https://github.com/someone/new-skills' } }]) From c41e2336ba67620bc5d4d885a3823f9e1e344e2e Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 8 Oct 2026 19:31:31 +1100 Subject: [PATCH 3/4] feat(demos): record good-css reading list --- layers/registry/server/data/skill-demos.json | 40 +++ .../vojtaholik/good-css/good-css/index.html | 315 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 layers/registry/server/demos/vojtaholik/good-css/good-css/index.html diff --git a/layers/registry/server/data/skill-demos.json b/layers/registry/server/data/skill-demos.json index f7cd5b14..70577bd6 100644 --- a/layers/registry/server/data/skill-demos.json +++ b/layers/registry/server/data/skill-demos.json @@ -1389,6 +1389,46 @@ } ] }, + { + "owner": "vojtaholik", + "repo": "good-css", + "name": "good-css", + "makes": "ui-component", + "authorName": null, + "sourceUrl": "https://github.com/vojtaholik/good-css/blob/main/skills/good-css/SKILL.md", + "prompt": "Build a polished responsive reading-list component for a fictional independent design journal called Margin. Show six article cards with different title lengths, categories, reading times, and CSS-drawn editorial cover art. Include working category filters, a light/dark theme switch, and bookmark toggles with an in-page saved count. Use the modern CSS techniques taught by this Skill to make the same component work in a narrow sidebar and a wide main column. Add a simple width control so visitors can see it adapt. Keep the reading list the main visual focus. Use warm paper, strong editorial typography, and restrained vermilion accents. No invented testimonials or statistics. Save one self-contained index.html with inline CSS and runtime JavaScript. No external assets or dependencies. Use TypeScript for any tooling. The demo runs in an opaque iframe with sandbox allow-scripts only: no storage, form submissions, popups, or downloads. Every control must work by keyboard. Support reduced motion and widths from 390px to 1440px. Do not ask questions.", + "agent": "Codex", + "agentVersion": "0.161.0", + "model": "gpt-6.1-sol", + "effort": "medium", + "skillCommit": "e777d97100d184cc78ffea6c5603f409610d5ea4", + "recordedAt": "2026-10-08", + "outputFile": "index.html", + "shots": [ + { + "file": "desktop-90180cd258.jpg", + "width": 1440, + "height": 1672, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Build a polished responsive reading-list component for a fictional independent design journal called Margin. Show six ar", + "poster": { + "file": "desktop-poster-4f0dd2b501.jpg", + "height": 900 + } + }, + { + "file": "mobile-e7b77d8d88.jpg", + "width": 390, + "height": 1839, + "viewport": "mobile", + "alt": "Phone screenshot of the page the Agent built for: Build a polished responsive reading-list component for a fictional independent design journal called Margin. Show six ar", + "poster": { + "file": "mobile-poster-ae31c943c8.jpg", + "height": 844 + } + } + ] + }, { "owner": "yfe404", "repo": "tech-visualizer", diff --git a/layers/registry/server/demos/vojtaholik/good-css/good-css/index.html b/layers/registry/server/demos/vojtaholik/good-css/good-css/index.html new file mode 100644 index 00000000..824af994 --- /dev/null +++ b/layers/registry/server/demos/vojtaholik/good-css/good-css/index.html @@ -0,0 +1,315 @@ + + + + + + + +Margin · The reading list + + + +
+
+
Margin↗
+

An independent journal
of design & culture

+ +
+
+

For the curious, by the curious

Ideas worth
making room for.

+

On the things we make, the spaces we inhabit, and the ideas that stay with us.

Read slowly. Think widely.

+
+ +
+

The reading list

+

06 articles

+
+
+ +

Design

The quiet power of doing less

What happens when we stop adding, and start paying attention?

+

6 min read Essay

+
+
+ +

Typography

A typeface is a voice before a word is read

The subtle ways letterforms shape what we hear on the page.

+

8 min read Perspective

+
+
+ +

Culture

In praise of the ordinary

Finding something remarkable in the things we walk past every day.

+

4 min read Observation

+
+
+ +

Spaces

Rooms for thinking

A window, a chair, a little daylight. The architecture of a good idea.

+

5 min read Field notes

+
+
+ +

Design

The things we make, and the things that make us

Tracing the invisible connections between our lives and our work.

+

11 min read Long read

+
+
+ +

Culture

Stay curious.

An open mind is a practice. Here is a place to begin.

+

3 min read Short thought

+
+
+ +
A good place to pause
+
+

Margin   An independent point of view.

A fictional journal. A real moment to slow down.

+
+

+ + + From e35028e07d4803346303048a8b74df4b25a75c17 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 8 Oct 2026 19:32:52 +1100 Subject: [PATCH 4/4] style(test): use braces for submission cancellation cases --- test/unit/home-search.nuxt.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/home-search.nuxt.test.ts b/test/unit/home-search.nuxt.test.ts index 52037109..411eb1f4 100644 --- a/test/unit/home-search.nuxt.test.ts +++ b/test/unit/home-search.nuxt.test.ts @@ -124,10 +124,12 @@ describe('homepage search interactions', () => { stateOverride.value = { _tag: 'loading', rows: [] } const wrapper = await mountSearch() await press(wrapper, 'Enter') - if (action === 'edit') + if (action === 'edit') { await wrapper.get('input').setValue('vue') - else if (action === 'Escape') + } + else if (action === 'Escape') { await press(wrapper, 'Escape') + } else { stateOverride.value = { _tag: 'error', error: new Error('offline') } await flushPromises()