Constraints agreed in the design comments
Header cluster order: Explore - Chats - Notifications - Activity Feed - Bookmarks - Avatar.
Icons only, no text labels, tooltips for naming.
Bookmarks is desktop only, the web cluster omits it. Chats is a separate project so I will leave a slot
Clicking the icon scopes Explore to the currently open space. All spaces is an opt-in widening.
Implementation steps
Header button and breadcrumb
Add the Explore icon to the top-right cluster in packages/ui/src/site-header.tsx, beside the existing Activity Feed button. Wire it to {key: 'explore', context: {type: 'site', id: <current space>}}. Add the Home > Explore breadcrumb to the page.
Desktop gets the same icon in its header, same route.
All-spaces scope and its route
routeToUrl returns null for context.type !== 'site'. Return /hm/explore and query string for the node case. routeToHref delegates here, so verify it stops inheriting the null.
hypermediaUrlToRoute: add an early match for /hm/explore returning context: {type: 'node'}.
New route apps/web/app/routes/hm.explore.tsx to /hm/explore?q=…&sort=…. Copy the loader from hm.notifications.tsx plus a meta for title and favicon. Body is client-only.
Existing <space>/:explore URLs keep working. Scope switching is then onScopeChange flipping context, which the scope pill already supports.
The reason is that the node scope is in the route schema but has no web URL, so "All spaces" is unreachable until this exists.
Filter chip UI
Replace the query string bar in <ExplorePage> with the design's chip row: All spaces / All types / In / Author / Publication date / See all filters, plus the Sort dropdown.
Build the chips as views over the existing AST - exploreQueryChips, toggleExplorePredicate, removeExploreQueryChip, clearExploreConditions and cycleExploreSort are already exported from packages/shared/src/explore.ts. Keep the raw query string behind "See all filters" as the power path.
Facet mapping:
Chip | Maps to | Work |
|---|---|---|
All types |
| UI only for document/block/comment |
In |
| UI only |
All spaces |
| One space at a time is free. multi select needs a decision |
Publication date |
| Works, but as an ISO string compare. ranges need care |
Author |
| Backend work needed so ship without it |
Sort |
| UI only |
Spaces and People facets
Add 'space' to HMExploreResultType and to the resultTypes set so type:space parses. Map ['space'] to [EntityKindFilter.ENTITY_KIND_SPACE] in entityKindFilters(). In assembleExploreResults, classify a document result with no path as a space. Add the tab to tabs in explore-page.tsx.
Add contact the same way, letting it through contentTypeFilters() / entityKindFilters().
The daemon already indexes both. ENTITY_KIND_SPACE is in the default entityKindFilters() list today, so spaces are already in the result set, just rendered as ordinary documents. Contacts likewise (CONTENT_TYPE_CONTACT, ENTITY_KIND_CONTACT), and api-search.ts already maps type: 'contact'.
One design question: a space row probably wants its own presentation: space icon, member count, document count, rather than the document row.
Landing state (empty query)
Two blocks, both built on the row anatomy from the old desktop Library page:
Your Spaces - spaces the account joined, from contacts with subscribe.site: true. Card: icon, name, role badge, document count, last updated. Counts and times from queryDocuments scoped with SpaceMatch. the role badge from useIsSiteOwner.
Recently Updated Documents - breadcrumb, title, FacePile, comment/reference counters, and LibraryEntryUpdateSummary (packages/ui/src/activity.tsx). apps/desktop/src/models/library.ts has the shape and the comment-hydration trick, but calls grpcClient directly. The web version must go through useUniversalClient(). apps/web/app/local-db-recents.ts is a good secondary source for a "Jump back in" strip.
The "Jump to:" chip row above them just sets the type facet on an empty query.
Rows must carry PrivateBadge and respect visibility, as the Library rows did.
Per-facet counts
The design shows exact counts (Documents 51, Text Blocks 11). assembleExploreResults computes counts from loaded pages after client-side filtering, so the numbers grow as you scroll and the intersection path caps at EXPLORE_INTERSECTION_DOCUMENT_CAP = 1000.
We can ship approximate counts first, but needs backend proto change for accurate count.
Questions
Media and citations - no type in the FTS index (title, document, comment, contact, profile, meta only), so they cannot be searched like other facets. I don't know how this can be implemented
Attributes as a result type - predicates and autocomplete exist but results do not.
Ask Agent toggle - is this something I should do?
Multi space selection. Search takes one iri_filter, and searchIriFilter() bails when a query has more than one positive scope. Restrict the facet to one space, do multiple searches and merge client-side, or make iri_filter repeated in the proto?
Author facet. No daemon predicate exists. Client-side filtering only sees the loaded page.
Desktop's existing entry points. The Explore results for "…" / Advanced search footer rows in the desktop search popover become a second path once the icon exists. Should we keep them or remove?
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime