Public: anyone can read this; the crew can change it.
The explorer keeps its filters in the URL, so a filtered view is a link that reloads the same #10732
Check · Review · Land · Live done — Landed and live in production.
The explorer's filters are now separate query parameters: `/explore?kind=app&sort=forks&owner=olive&updated=7d&archived=yes&q=garden`. Reloading one gives the same page. I ran `tsc`, the gate and the explore, MCP-size and related tests locally, and all pass. I did not run the full suite; the forge runs that on push. The `keep_plan` tool wasn't available to me, so there is no plan on the issue.
shipped production is serving it deploy f9da504
The explorer's filters are now separate query parameters: /explore?kind=app&sort=forks&owner=olive&updated=7d&archived=yes&q=garden. Reloading one gives the same page. I ran tsc, the gate and the explore, MCP-size and related tests locally, and all pass. I did not run the full suite; the forge runs that on push. The keep_plan tool wasn't available to me, so there is no plan on the issue.
What changed
src/actions/explore.tsgainsexploreLineFrom, which turns a request'sqplus the separate parameters into the existing one-line form.exploreParamsdoes the reverse for links./exploreand/api/v1/exploreread the parameters throughexploreLineFrom. The page's chips, "Drop a term" links and "Did you mean" link now use the separate-parameter form. The page is still server-rendered from the URL.- The MCP
exploretool takeskind,sort,updated,ownerandarchivedas arguments, named the same as the URL parameters. A parameter wins over the same key typed inq.test/mcp-size.test.tspasses, no ceiling raised. - A value is cut to one word, so
kind=app gardencan't add text or another key. An invalid value is dropped for the default and named in a note. As before, the page and API answer 422 when a value is invalid, and the page still shows the default results. Unknown parameters are ignored. Output stays escaped. - Ranking is untouched, and
explore-rankandexplore-paritypass. test/explore-url.test.tsis new. It requests two filtered URLs and checks they return different result sets, that a reload equals the first load, and that unknown parameters, bad values, escaping and the chip links behave as above.docs/MCP.mdis updated.
Existing tests I changed
- Several tests in
explore.test.ts,explore-unknown-words.test.tsandexplore-parity.test.tsasserted the oldhref="/explore?q=kind%3Aapp"link form. They now expect the separate-parameter form. I changed them because the link format is what this issue changes. The?q=line form still works on the page, API and MCP, and the search box still submits it.
What I chose not to do
- No in-page
pushStatefiltering. The filters are plain links, so Back already returns to the previous filter and there is no script to ship. - I did not add a way for the search box to submit separate parameters. It still puts the whole line into
q.
To try it
- Open
/explore?kind=app&sort=forks, then/explore?kind=person, and reload either. - Run
bun test test/explore-url.test.ts.
I had to add an empty commit with a "Try:" line because the pre-push hook asked for one.
The URL side is sound: server-rendered from separate params, values cut to one word, escaped, unknown params ignored. Two behaviours the pull adds have no test, and box 4 rests on one of them. Fix first: add an MCP test for the new arguments.
- MCP: no test calls the explore tool with kind/sort/updated/owner/archived as arguments. Add one that asserts the same items as the API for the same params (in test/explore-parity.test.ts). Without it, dropping the
...Object.fromEntries(EXPLORE_PARAMS...)schema line or the str(args[name]) lookup stays green. - Precedence: docs/MCP.md says a parameter wins over the same key in q, but nothing tests it. exploreLineFrom just appends
kind:appafter q, so q=kind:person with kind=app depends on how readExploreLine resolves duplicate keys. Add a test on page, API and MCP that proves the parameter wins, or fix the code so it does.
Boxes 1, 3 and 5 are met. Box 2 is met by plain links (Back works, no script). Box 4 is left until finding 1 is fixed.
Both findings are fixed and pushed. tsc, scripts/gate.ts and the explore tests pass: test/explore-parity.test.ts plus the five other explore test files I ran.
What changed
- Precedence is now explicit.
exploreLineFrominsrc/actions/explore.tsdrops akey:word typed inqwhen a separate parameter of that key is given. Before this, a duplicate key resolved by the order the reader happened to scan the line. The parameter now wins whatever the order, with no change to the line grammar. The one-word cut on values is unchanged. - The MCP schema is tighter. The new arguments carry
maxLength: 100, andkind,sort,updatedandarchivedcarry anenumtaken from the same word lists the parser uses (EXPLORE_VALUES).owneris a free name, so it has no enum. - MCP test (finding 1). In
test/explore-parity.test.ts, theexploretool is called withkind,sort,updated,ownerandarchivedas arguments. Four parameter sets are checked: the API, the MCP, the page and the equivalent one-lineqall return the same rows in the same order. Dropping the schema line or thestr(args[name])lookup would now fail this. - Precedence test (finding 2). The test sends
q=kind:person sort:forkstogether withkind=app&sort=name. All three surfaces return the app-only, name-sorted rows. On the page I checked the row count and that every row is an app link, not the exact order.
Not done
- I did not check that the MCP server enforces the
enum. It looks like the server does not validate arguments againstinputSchema, so the enum is documentation for clients. An unknown value still returns the existing 422unknown_filtererror, which suggests a fix, and I kept that path. scripts/fast.tssaid the diff was too broad to name tests, so I picked the explore suites myself and did not run the full suite.
Try it
Open /explore?kind=app&sort=forks, or call the explore tool with { "kind": "app", "sort": "name" }.
The explorer's filters are now separate URL parameters (?kind=app&sort=forks&owner=x&updated=7d&archived=yes) on the page, the API and the MCP explore tool. A filtered view is a link that reloads the same. tsc and the touched tests pass; I did not re-run the full suite. The forge's check will run it.
What changed
exploreLineFrominsrc/actions/explore.tsbuilds one line fromqplus each parameter as a one-wordkey:value. A parameter wins over the same key typed inq: that word is dropped fromq, so the result no longer depends on the order the line is read.- The MCP
exploretool takeskind,sort,updated,ownerandarchivedas arguments, each capped at 100 characters. test/explore-parity.test.tscalls the tool with those arguments and checks it returns the same rows as the API, the page and the one-line form. A second test sendsq=kind:person sort:forkswithkind=app&sort=nameand checks all three surfaces return the app-only, name-sorted rows.
Fix after the failed check
- Cause: the value enums I had added to the
exploreschema made the tool larger, which pushedforge_viewonto the secondtools/listpage. That is what failedmcp-forge-view. - Fix: I removed the enums from the listing, so the arguments carry only the length cap. An unknown value still returns the existing 422
unknown_filtererror that names the allowed words. This leaves out the enum the reviewer asked for, because the listing is paged by size (20 KB a page) and the test needsforge_viewon page one. - Flaky test:
pull-quiet("the pull list wears a dot per row…") passed when I ran it here, and I did not find a cause. It looks like a flake in that shard.
What I chose not to do
- No client-side script. Filters are plain links, so Back works.
To try it
Open /explore?kind=app&sort=forks and reload it, or call the explore tool with { "kind": "app", "sort": "name" }.
The behaviour works in the preview: /explore?kind=app&sort=forks answers 200 and the parameters are tested on the page, API and MCP. One thing to fix first: docs/features/explore.md still describes the old form. Line 31 says 'The page's chips are plain links to /explore?q=<line>', and the table at lines 35-39 shows ?q=sort:forks, ?q=kind:app and so on. This change makes the chips separate parameters (?sort=forks) and adds kind, sort, updated, owner and archived as MCP arguments, so the how-to has to say that. Update it. Earlier asks: the MCP argument test and the precedence test are met. The dropped enum is acceptable given the tools/list size budget. Non-blocking: the precedence test on the page checks only row count and link shape, not exact order.
/explore now keeps each filter in its own query parameter (?kind=app&sort=forks&owner=x), so a filtered view is a link that reloads the same. The page's links are built in this form, and GET /api/v1/explore and the MCP explore tool accept the same names. The only argument the tool had before was the single q line, which still works everywhere, so old /explore?q=… links keep working.
src/actions/explore.ts:exploreLineFromturns the request'sqplus the separate parameterskind,sort,updated,ownerandarchivedinto the one line the explorer already reads. A separate parameter overrides the same key typed inq, so?q=sort:name&sort=forkssorts by forks. Each value is one word: whitespace is removed and it is cut at 100 characters, so it can't sneak in a second key. A value the explorer doesn't know gets the same 422 as an unknown word inq.exploreParamswrites a query back out as separate parameters, which is the form the page links to.- Page, API and MCP: the three entry points (
src/web/studio/explore.ts,src/http/api.ts,src/http/mcp/tools/explore.ts) all read throughexploreLineFrom. The page's tabs, chips, owner links and "Did you mean" link use the separate parameters. On MCP, the five keys are their own string arguments without the list of allowed values: adding it would pushforge_viewoff the first page oftools/list. The terminal's search line still takes only the single line. - Docs: in
docs/features/explore.md, as asked in review, the table now shows the parameter form (?sort=forks,{ sort: "forks" }). It also says theqline still works, that a parameter wins over the same key inq, and that the "Did you mean" link uses the parameter form.docs/MCP.mddescribes the new arguments. - Tests:
test/explore-url.test.tscovers the URL form and the precedence rule.test/explore-parity.test.tschecks that the page, API and MCP return the same rows for the separate parameters. The test on the page that a parameter beats the same key inqchecks only the number of rows and the shape of the links, not the exact order; I left that as it was.
The gate passes, the type check is clean, and the explore, MCP-size and docs tests pass (99 tests across 8 files). scripts/fast.ts named 1680 test files for this branch, which is over its five-minute budget, so I chose those eight myself instead.
To try it: open /explore?kind=app&sort=forks, reload it, and click a chip; the address keeps the separate parameters. Compare /explore?q=sort:name&sort=forks, which sorts by forks, and GET /api/v1/explore?kind=person.
🤖 Generated with Claude Code
Earlier asks met: MCP arguments are tested against API and page, and a parameter now wins over the same key in q on all three surfaces; docs table shows the parameter form. Dropping the enum to keep forge_view on page one of tools/list is reasonable; an unknown value still gets the 422 naming the allowed words. The preview works: chips are /explore?kind=app and ?sort=forks, a bad sort gives 422 with the default rows, and the API takes the same parameters. The change adds no markup, so the design rules are unchanged. Non-blocking: the precedence test on the page checks only the row count and link shape, not the order.
The explorer's filters are now separate query parameters:
/explore?kind=app&sort=forks&owner=olive&updated=7d&archived=yes&q=garden. Reloading one gives the same page. I rantsc, the gate and the explore, MCP-size and related tests locally, and all pass. I did not run the full suite; the forge runs that on push. Thekeep_plantool wasn't available to me, so there is no plan on the issue.What changed
src/actions/explore.tsgainsexploreLineFrom, which turns a request'sqplus the separate parameters into the existing one-line form.exploreParamsdoes the reverse for links./exploreand/api/v1/exploreread the parameters throughexploreLineFrom. The page's chips, "Drop a term" links and "Did you mean" link now use the separate-parameter form. The page is still server-rendered from the URL.exploretool takeskind,sort,updated,ownerandarchivedas arguments, named the same as the URL parameters. A parameter wins over the same key typed inq.test/mcp-size.test.tspasses, no ceiling raised.kind=app gardencan't add text or another key. An invalid value is dropped for the default and named in a note. As before, the page and API answer 422 when a value is invalid, and the page still shows the default results. Unknown parameters are ignored. Output stays escaped.explore-rankandexplore-paritypass.test/explore-url.test.tsis new. It requests two filtered URLs and checks they return different result sets, that a reload equals the first load, and that unknown parameters, bad values, escaping and the chip links behave as above.docs/MCP.mdis updated.Existing tests I changed
explore.test.ts,explore-unknown-words.test.tsandexplore-parity.test.tsasserted the oldhref="/explore?q=kind%3Aapp"link form. They now expect the separate-parameter form. I changed them because the link format is what this issue changes. The?q=line form still works on the page, API and MCP, and the search box still submits it.What I chose not to do
pushStatefiltering. The filters are plain links, so Back already returns to the previous filter and there is no script to ship.q.To try it
/explore?kind=app&sort=forks, then/explore?kind=person, and reload either.bun test test/explore-url.test.ts.I had to add an empty commit with a "Try:" line because the pre-push hook asked for one.
Closes #10727