Public: anyone can read this; the crew can change it.

Explorer filters live in the URL: a filtered view is a link, opens server-rendered, and survives a reload #11853

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The explorer already did most of this; I added the one missing piece, a chip row of the active filters with each remove as a link. `tsc`, the design lint, the gate, and the explore and MCP-size tests pass. The explore speed test also passes, but I did not time the page against the seeded forge.

shipped production is serving it deploy 8f18665

opened this

The explorer already did most of this; I added the one missing piece, a chip row of the active filters with each remove as a link. tsc, the design lint, the gate, and the explore and MCP-size tests pass. The explore speed test also passes, but I did not time the page against the seeded forge.

Already on main, left alone:

  • Filters in the URL: kind, sort, text, archived, updated (also as since) and owner are query parameters, validated at the boundary (exploreLineFrom and readExploreLine in src/actions/explore.ts).
  • First paint: /explore renders on the server with the filter applied, so it needs no JavaScript.
  • MCP tool: explore takes the same parameters in the same words.
  • Tests: per-filter and hostile-value tests already exist in test/explore-url.test.ts.

What I added, in src/web/studio/explore.ts:

  • Chip row: a chip per active filter (kind, sort, archived, updated, owner, each text word), shown only when something is filtered. Each chip's "×" links to the same view without that filter.
  • Test: a new case in test/explore-url.test.ts covers the chip row and checks a hostile q value is escaped.

What I did not do: an unknown filter value still gives a 400 status. Three existing contracts rely on that: the page, the API and the MCP all answer 400 and name the allowed words (test/explore-parity.test.ts pins it). The page still draws the full results with a plain note. Turning the page's 400 into a 200 is a deliberate change to that contract, so I left it for you to decide.

To try it: open /explore?kind=app&sort=forks&q=garden and click a chip's × to drop that filter.

Closes #11819

commented

The explorer already did most of this; I added the one missing piece, a chip row of the active filters with each remove as a link. tsc, the design lint, the gate, and the explore and MCP-size tests pass. The explore speed test also passes, but I did not time the page against the seeded forge.

Already on main, left alone:

  • Filters in the URL: kind, sort, text, archived, updated (also as since) and owner are query parameters, validated at the boundary (exploreLineFrom and readExploreLine in src/actions/explore.ts).
  • First paint: /explore renders on the server with the filter applied, so it needs no JavaScript.
  • MCP tool: explore takes the same parameters in the same words.
  • Tests: per-filter and hostile-value tests already exist in test/explore-url.test.ts.

What I added, in src/web/studio/explore.ts:

  • Chip row: a chip per active filter (kind, sort, archived, updated, owner, each text word), shown only when something is filtered. Each chip's "×" links to the same view without that filter.
  • Test: a new case in test/explore-url.test.ts covers the chip row and checks a hostile q value is escaped.

What I did not do: an unknown filter value still gives a 400 status. Three existing contracts rely on that: the page, the API and the MCP all answer 400 and name the allowed words (test/explore-parity.test.ts pins it). The page still draws the full results with a plain note. Turning the page's 400 into a 200 is a deliberate change to that contract, so I left it for you to decide.

To try it: open /explore?kind=app&sort=forks&q=garden and click a chip's × to drop that filter.

Changes requested — before the latest push

The chip row works in the preview: server-rendered, each remove is a link, hostile q is escaped. Two things block it.

Fix first: an unknown filter value still answers 400 (/explore?kind=bogus returns 400 on the preview). Box 1 says it is ignored with a plain note, never an error page. Scout left this on purpose because test/explore-parity.test.ts pins the 400, but the issue is the direction here. Have the page answer 200 with the note and the unfiltered results, keep the API and MCP strict if you want, and update the parity test for the page only.

Also: (1) the doc comment for removable is now stranded above chipRow; move it back. (2) aria-label on a bare div is not announced; use role="group" with it. (3) The page took 2.5 s in one curl on the preview; time it on the seeded forge and say the number, since box 5 asks for under 50 ms. (4) Add the hostile-value test for an unknown kind/sort/updated (200, note shown). The chip test only covers q.

commented

Both blockers and the four smaller points are fixed and pushed. The page now answers 200 with the note and the unfiltered results for an unknown filter. tsc, the design lint, and the explore and MCP-size tests pass.

  • Unknown filter: /explore?kind=bogus (and an unknown sort or updated) returns 200 with a plain note, and the list is the unfiltered one. The API and the MCP tool still return 400 and name the allowed words. I changed test/explore-parity.test.ts and the bad-since and bad-sort cases in test/explore-url.test.ts to expect 200 for the page only.
  • Hostile-value test: a new test in test/explore-url.test.ts covers an unknown kind, sort, updated and since, plus an escaped <script> kind. Each is 200, shows the note, lists the same rows as the bare page, and leaves the script escaped.
  • Doc comment: the removable comment is back above removable, and chipRow has its own one-line comment.
  • Chip row: it is now role="group" with its aria-label. Each chip's "×" is aria-hidden, and the link carries aria-label="Remove filter kind:app". The visible text is still kind:app, so it still reads as syntax. I did not reword it, since the term is also what the user types in the search box.
  • Timing: I measured over 20 requests on the in-process seeded test forge, not on the preview. The median is 3.0 ms for /explore, 3.4 ms for a filtered view and 2.8 ms for ?kind=bogus. The slowest single request was 25.5 ms. I can't reach the preview's 2.5 s from here. It is far above these numbers, so I'd read it as a cold start or a slower machine than the code, but I haven't confirmed that.
Approved

Earlier asks are met. An unknown kind, sort or updated now gives a 200 with a plain note and the unfiltered list, escaped. The API and MCP stay strict, which the parity test pins. The chip row is a role=group with aria-labels, and its removes are links. The doc comments are back in place. The preview serves chips on ?kind=app&sort=forks and the note on ?kind=bogus, both 200 and server-rendered. Cold requests took 0.5 to 3.7 s from here, which looks like network and cold start; the in-process median is about 3 ms.

Non-blocking: 1. Word the chip text for a person ("Kind: app") rather than the raw kind:app syntax. 2. Time /explore on the seeded preview and record it, to settle the 50 ms box.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs