Skip to content

Commit aa86725

Browse files
aaronpowellCopilotCopilot
authored
Add URL-synced listing search (#1217)
* Add URL-synced listing search Closes #1174 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1edf5bc commit aa86725

9 files changed

Lines changed: 401 additions & 17 deletions

File tree

website/src/scripts/choices.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export function getChoicesValues(choices: Choices): string[] {
1212
return Array.isArray(val) ? val : (val ? [val] : []);
1313
}
1414

15+
/**
16+
* Restore selected values on a Choices instance.
17+
*/
18+
export function setChoicesValues(choices: Choices, values: string[]): void {
19+
// Clear any existing active items so that the final selection matches `values`
20+
choices.removeActiveItems();
21+
// Set all provided values as the current selection
22+
choices.setChoiceByValue(values);
23+
}
24+
1525
/**
1626
* Create a new Choices instance with sensible defaults
1727
*/

website/src/scripts/pages/agents.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
/**
22
* Agents page functionality
33
*/
4-
import { createChoices, getChoicesValues, type Choices } from '../choices';
4+
import {
5+
createChoices,
6+
getChoicesValues,
7+
setChoicesValues,
8+
type Choices,
9+
} from '../choices';
510
import { FuzzySearch, type SearchItem } from '../search';
6-
import { fetchData, debounce, setupDropdownCloseHandlers, setupActionHandlers } from '../utils';
11+
import {
12+
fetchData,
13+
debounce,
14+
getQueryParam,
15+
getQueryParamFlag,
16+
getQueryParamValues,
17+
setupDropdownCloseHandlers,
18+
setupActionHandlers,
19+
updateQueryParams,
20+
} from '../utils';
721
import { setupModal, openFileModal } from '../modal';
822
import { renderAgentsHtml, sortAgents, type AgentSortOption, type RenderableAgent } from './agents-render';
923

@@ -111,6 +125,16 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
111125
resourceListHandlersReady = true;
112126
}
113127

128+
function syncUrlState(searchInput: HTMLInputElement | null): void {
129+
updateQueryParams({
130+
q: searchInput?.value ?? '',
131+
model: currentFilters.models,
132+
tool: currentFilters.tools,
133+
handoffs: currentFilters.hasHandoffs,
134+
sort: currentSort === 'title' ? '' : currentSort,
135+
});
136+
}
137+
114138
export async function initAgentsPage(): Promise<void> {
115139
const list = document.getElementById('resource-list');
116140
const searchInput = document.getElementById('search-input') as HTMLInputElement;
@@ -132,35 +156,67 @@ export async function initAgentsPage(): Promise<void> {
132156
// Initialize Choices.js for model filter
133157
modelSelect = createChoices('#filter-model', { placeholderValue: 'All Models' });
134158
modelSelect.setChoices(data.filters.models.map(m => ({ value: m, label: m })), 'value', 'label', true);
159+
160+
const initialQuery = getQueryParam('q');
161+
const initialModels = getQueryParamValues('model').filter(model => data.filters.models.includes(model));
162+
const initialTools = getQueryParamValues('tool').filter(tool => data.filters.tools.includes(tool));
163+
const initialSort = getQueryParam('sort');
164+
165+
if (searchInput) searchInput.value = initialQuery;
166+
if (initialModels.length > 0) {
167+
currentFilters.models = initialModels;
168+
setChoicesValues(modelSelect, initialModels);
169+
}
170+
135171
document.getElementById('filter-model')?.addEventListener('change', () => {
136172
currentFilters.models = getChoicesValues(modelSelect);
137173
applyFiltersAndRender();
174+
syncUrlState(searchInput);
138175
});
139176

140177
// Initialize Choices.js for tool filter
141178
toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' });
142179
toolSelect.setChoices(data.filters.tools.map(t => ({ value: t, label: t })), 'value', 'label', true);
180+
if (initialTools.length > 0) {
181+
currentFilters.tools = initialTools;
182+
setChoicesValues(toolSelect, initialTools);
183+
}
143184
document.getElementById('filter-tool')?.addEventListener('change', () => {
144185
currentFilters.tools = getChoicesValues(toolSelect);
145186
applyFiltersAndRender();
187+
syncUrlState(searchInput);
146188
});
147189

148190
// Initialize sort select
191+
if (initialSort === 'lastUpdated') {
192+
currentSort = initialSort;
193+
if (sortSelect) sortSelect.value = initialSort;
194+
}
149195
sortSelect?.addEventListener('change', () => {
150196
currentSort = sortSelect.value as AgentSortOption;
151197
applyFiltersAndRender();
198+
syncUrlState(searchInput);
152199
});
153200

154201
const countEl = document.getElementById('results-count');
155202
if (countEl) {
156203
countEl.textContent = `${allItems.length} of ${allItems.length} agents`;
157204
}
158205

159-
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
206+
searchInput?.addEventListener('input', debounce(() => {
207+
applyFiltersAndRender();
208+
syncUrlState(searchInput);
209+
}, 200));
210+
211+
if (getQueryParamFlag('handoffs')) {
212+
currentFilters.hasHandoffs = true;
213+
if (handoffsCheckbox) handoffsCheckbox.checked = true;
214+
}
160215

161216
handoffsCheckbox?.addEventListener('change', () => {
162217
currentFilters.hasHandoffs = handoffsCheckbox.checked;
163218
applyFiltersAndRender();
219+
syncUrlState(searchInput);
164220
});
165221

166222
clearFiltersBtn?.addEventListener('click', () => {
@@ -172,8 +228,10 @@ export async function initAgentsPage(): Promise<void> {
172228
if (searchInput) searchInput.value = '';
173229
if (sortSelect) sortSelect.value = 'title';
174230
applyFiltersAndRender();
231+
syncUrlState(searchInput);
175232
});
176233

234+
applyFiltersAndRender();
177235
setupModal();
178236
setupDropdownCloseHandlers();
179237
setupActionHandlers();

website/src/scripts/pages/hooks.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
/**
22
* Hooks page functionality
33
*/
4-
import { createChoices, getChoicesValues, type Choices } from "../choices";
4+
import {
5+
createChoices,
6+
getChoicesValues,
7+
setChoicesValues,
8+
type Choices,
9+
} from "../choices";
510
import { FuzzySearch, type SearchItem } from "../search";
611
import {
712
fetchData,
813
debounce,
14+
getQueryParam,
15+
getQueryParamValues,
916
showToast,
1017
downloadZipBundle,
18+
updateQueryParams,
1119
} from "../utils";
1220
import { setupModal, openFileModal } from "../modal";
1321
import {
@@ -126,6 +134,15 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
126134
resourceListHandlersReady = true;
127135
}
128136

137+
function syncUrlState(searchInput: HTMLInputElement | null): void {
138+
updateQueryParams({
139+
q: searchInput?.value ?? "",
140+
hook: currentFilters.hooks,
141+
tag: currentFilters.tags,
142+
sort: currentSort === "title" ? "" : currentSort,
143+
});
144+
}
145+
129146
async function downloadHook(
130147
hookId: string,
131148
btn: HTMLButtonElement
@@ -210,9 +227,26 @@ export async function initHooksPage(): Promise<void> {
210227
"label",
211228
true
212229
);
230+
231+
const initialQuery = getQueryParam("q");
232+
const initialHooks = getQueryParamValues("hook").filter((hook) =>
233+
data.filters.hooks.includes(hook)
234+
);
235+
const initialTags = getQueryParamValues("tag").filter((tag) =>
236+
data.filters.tags.includes(tag)
237+
);
238+
const initialSort = getQueryParam("sort");
239+
240+
if (searchInput) searchInput.value = initialQuery;
241+
if (initialHooks.length > 0) {
242+
currentFilters.hooks = initialHooks;
243+
setChoicesValues(hookSelect, initialHooks);
244+
}
245+
213246
document.getElementById("filter-hook")?.addEventListener("change", () => {
214247
currentFilters.hooks = getChoicesValues(hookSelect);
215248
applyFiltersAndRender();
249+
syncUrlState(searchInput);
216250
});
217251

218252
// Setup tag filter
@@ -225,20 +259,33 @@ export async function initHooksPage(): Promise<void> {
225259
"label",
226260
true
227261
);
262+
if (initialTags.length > 0) {
263+
currentFilters.tags = initialTags;
264+
setChoicesValues(tagSelect, initialTags);
265+
}
228266
document.getElementById("filter-tag")?.addEventListener("change", () => {
229267
currentFilters.tags = getChoicesValues(tagSelect);
230268
applyFiltersAndRender();
269+
syncUrlState(searchInput);
231270
});
232271

272+
if (initialSort === "lastUpdated") {
273+
currentSort = initialSort;
274+
if (sortSelect) sortSelect.value = initialSort;
275+
}
233276
sortSelect?.addEventListener("change", () => {
234277
currentSort = sortSelect.value as HookSortOption;
235278
applyFiltersAndRender();
279+
syncUrlState(searchInput);
236280
});
237281

238282
applyFiltersAndRender();
239283
searchInput?.addEventListener(
240284
"input",
241-
debounce(() => applyFiltersAndRender(), 200)
285+
debounce(() => {
286+
applyFiltersAndRender();
287+
syncUrlState(searchInput);
288+
}, 200)
242289
);
243290

244291
clearFiltersBtn?.addEventListener("click", () => {
@@ -249,6 +296,7 @@ export async function initHooksPage(): Promise<void> {
249296
if (searchInput) searchInput.value = "";
250297
if (sortSelect) sortSelect.value = "title";
251298
applyFiltersAndRender();
299+
syncUrlState(searchInput);
252300
});
253301

254302
setupModal();

website/src/scripts/pages/instructions.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
/**
22
* Instructions page functionality
33
*/
4-
import { createChoices, getChoicesValues, type Choices } from '../choices';
4+
import {
5+
createChoices,
6+
getChoicesValues,
7+
setChoicesValues,
8+
type Choices,
9+
} from '../choices';
510
import { FuzzySearch, type SearchItem } from '../search';
6-
import { fetchData, debounce, setupDropdownCloseHandlers, setupActionHandlers } from '../utils';
11+
import {
12+
fetchData,
13+
debounce,
14+
getQueryParam,
15+
getQueryParamValues,
16+
setupDropdownCloseHandlers,
17+
setupActionHandlers,
18+
updateQueryParams,
19+
} from '../utils';
720
import { setupModal, openFileModal } from '../modal';
821
import {
922
renderInstructionsHtml,
@@ -93,6 +106,14 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
93106
resourceListHandlersReady = true;
94107
}
95108

109+
function syncUrlState(searchInput: HTMLInputElement | null): void {
110+
updateQueryParams({
111+
q: searchInput?.value ?? '',
112+
extension: currentFilters.extensions,
113+
sort: currentSort === 'title' ? '' : currentSort,
114+
});
115+
}
116+
96117
export async function initInstructionsPage(): Promise<void> {
97118
const list = document.getElementById('resource-list');
98119
const searchInput = document.getElementById('search-input') as HTMLInputElement;
@@ -112,22 +133,42 @@ export async function initInstructionsPage(): Promise<void> {
112133

113134
extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' });
114135
extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true);
136+
137+
const initialQuery = getQueryParam('q');
138+
const initialExtensions = getQueryParamValues('extension').filter(extension => data.filters.extensions.includes(extension));
139+
const initialSort = getQueryParam('sort');
140+
141+
if (searchInput) searchInput.value = initialQuery;
142+
if (initialExtensions.length > 0) {
143+
currentFilters.extensions = initialExtensions;
144+
setChoicesValues(extensionSelect, initialExtensions);
145+
}
146+
if (initialSort === 'lastUpdated') {
147+
currentSort = initialSort;
148+
if (sortSelect) sortSelect.value = initialSort;
149+
}
150+
115151
document.getElementById('filter-extension')?.addEventListener('change', () => {
116152
currentFilters.extensions = getChoicesValues(extensionSelect);
117153
applyFiltersAndRender();
154+
syncUrlState(searchInput);
118155
});
119156

120157
sortSelect?.addEventListener('change', () => {
121158
currentSort = sortSelect.value as InstructionSortOption;
122159
applyFiltersAndRender();
160+
syncUrlState(searchInput);
123161
});
124162

125163
const countEl = document.getElementById('results-count');
126164
if (countEl) {
127165
countEl.textContent = `${allItems.length} of ${allItems.length} instructions`;
128166
}
129167

130-
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
168+
searchInput?.addEventListener('input', debounce(() => {
169+
applyFiltersAndRender();
170+
syncUrlState(searchInput);
171+
}, 200));
131172

132173
clearFiltersBtn?.addEventListener('click', () => {
133174
currentFilters = { extensions: [] };
@@ -136,8 +177,10 @@ export async function initInstructionsPage(): Promise<void> {
136177
if (searchInput) searchInput.value = '';
137178
if (sortSelect) sortSelect.value = 'title';
138179
applyFiltersAndRender();
180+
syncUrlState(searchInput);
139181
});
140182

183+
applyFiltersAndRender();
141184
setupModal();
142185
setupDropdownCloseHandlers();
143186
setupActionHandlers();

0 commit comments

Comments
 (0)