Skip to content
KismetKismetDevelopers
llms.txt

The browse channel

View .md

On every Kismet site the search island, results grid, rail, map, and quick view must stay in sync: switching to Map shows the map, hovering a card lights its pin, and clicking a pin opens the quick view. The browse-channel contract keeps this one session — with three separable layers:

  1. Interactions EMIT specific events — past-tense facts in Object Action form (Map Pin Clicked). Elements never dispatch commands.
  2. An event map maps events to commands — plain data, shipped as defaultBrowseEventMap, overridable by editing the data.
  3. Commands own the state — each changes exactly one field, with exact rollback. State lives in one owner (BrowseState + useBrowseState(), from @kismet-tech/sdk/react), never in local component state.

Interaction events are declared once in the emit vocabulary (EMIT_VOCABULARY in @kismet-tech/sdk/contracts). emit() validates the payload against the vocabulary’s zod schema, notifies listeners, and forwards to the telemetry sink when one is configured — an invalid payload throws.

Event Payload Fired by
Map Pin Clicked { vacationRentalSlug } activating a [data-kismet-pin]
VacationRental Card Clicked { vacationRentalSlug } activating a [data-kismet-card]
Map Pin Hovered { vacationRentalSlug | null } pointer over / out of a pin
VacationRental Card Hovered { vacationRentalSlug | null } pointer over / out of a card or hover element
Layout Toggle Clicked { layout } the results layout control
Quick View Close Clicked {} the quick view’s close control
Booking Requested booking request the booking flow — tracking-only

Hover-out emits the same Hovered event with vacationRentalSlug: null. An event no map entry reacts to is tracking-only — it reaches analytics and never drives UI. Booking Requested is flagged trackingOnly in the vocabulary; the coverage check keeps the vocabulary and the default map honest (every interaction event is mapped, or flagged tracking-only).

import { emit } from '@kismet-tech/sdk';
emit('Map Pin Clicked', { vacationRentalSlug: 'fern-ridge-cabin' });

To listen from React, useKismetEvent is on the ./react entry:

'use client';
import { useKismetEvent } from '@kismet-tech/sdk/react';
function Analytics() {
useKismetEvent((event) => console.log(event.name, event.payload));
return null;
}

The map is plain data: command id → the events that fire it, plus a payload mapping (the input-map / keybinding pattern). The SDK ships defaultBrowseEventMap:

Command Fired by events
sheet.open Map Pin Clicked, VacationRental Card Clicked
results.setHovered Map Pin Hovered, VacationRental Card Hovered
results.setLayout Layout Toggle Clicked
sheet.close Quick View Close Clicked

<BrowseState> uses the default map unless you pass eventMap. Passing {} means every event still fires — and nothing reacts.

Commands are imperative, one per outcome, each changing exactly one store field, all reversible, all registered in the shared action dispatcher and projected to WebMCP — so a human control and a WebMCP agent drive the exact same transitions.

Command Input Changes
sheet.open { slug } openSlug
sheet.close {} openSlug → null
results.setLayout { layout: 'grid' | 'map' } layout
results.setHovered { slug | null } hoveredSlug
import { defaultKismetActionDispatcher } from '@kismet-tech/sdk';
// Own code (choosers, agents) dispatches commands directly:
await defaultKismetActionDispatcher.dispatch('sheet.open', { slug: 'fern-ridge-cabin' }, { source: 'webmcp' });

Ownership rule (conformance/browse-command-ownership): shared fields change only through commands — never by writing two fields at once, and every rollback restores the exact prior value, so A→B→A ends at A.

layout and openSlug persist through the canonical stay URL’s UI-only keys (layout, sheet) and are written shallow — a layout switch or quick-view open MUST NOT re-read results.

'use client';
import { BrowseState, useBrowseState } from '@kismet-tech/sdk/react';
<BrowseState resultVacationRentals={resultVacationRentals} openableVacationRentals={openableVacationRentals}>
<SearchIsland /> {/* emits events */}
<ResultsGrid /> {/* receiver */}
<StayMap /> {/* receiver */}
<BookableProductQuickView /> {/* receiver */}
</BrowseState>;
function StayMap() {
const { layout, hoveredSlug, openSlug, resultVacationRentals } = useBrowseState();
// Re-render on every browse change; render nothing when layout is 'grid'.
}

useBrowseState() exposes layout, hoveredSlug, openSlug, the two vacation rental sets, and three emit helpers for senders that live outside a joined element: emitLayoutToggle(layout) (the island’s layout control), emitQuickViewClose() (the sheet’s close control), and emitVacationRentalCardClick(slug) (a building-pin chooser row). Receivers never dispatch — a click that must change state emits its event and the event map reacts. For commands outside an interaction (a WebMCP tool, a chooser’s async flow), dispatch on the action dispatcher directly.

Mount BrowseState, render pins and cards with the join attributes, and give the island’s layout toggle an emit call. That is the whole integration — the default map opens the sheet from pin and card clicks, highlights on hover, and switches layouts.

Recipe: a pin that scrolls the list instead

Section titled “Recipe: a pin that scrolls the list instead”

Keep the pin’s emission, and remap the reaction — the event is the contract, the reaction is data. Drop the sheet.open entry from the default map so clicks stop opening the sheet, and let the site’s own code scroll the grid into view by listening with useKismetEvent:

'use client';
import { defaultBrowseEventMap } from '@kismet-tech/sdk';
// Hover, layout, and close keep their default reactions; pin and card
// clicks no longer open the sheet.
const { 'sheet.open': _removed, ...scrollToListEventMap } = defaultBrowseEventMap;
function ScrollToList() {
useKismetEvent((event) => {
if (event.name === 'Map Pin Clicked') scrollToGridCard(event.payload.vacationRentalSlug);
});
return null;
}
<BrowseState eventMap={scrollToListEventMap}>
<ScrollToList />
…
</BrowseState>;

Booking Requested is already in the vocabulary with trackingOnly: true; emit it from the booking flow and no event map may react to it. Add a new tracking-only event by appending a vocabulary entry with trackingOnly: true — the coverage check then guards it against accidental mapping.

Any element — a server-rendered card, a map pin, a plain button, a rail photo — joins the channel by carrying data-kismet-slug (itself or on the nearest ancestor) plus one join attribute:

  • data-kismet-pin — emits Map Pin Clicked / Map Pin Hovered;
  • data-kismet-card — emits VacationRental Card Clicked / VacationRental Card Hovered;
  • data-kismet-hover — a hover-only join (a rail photo), emitting VacationRental Card Hovered.

One delegated listener inside BrowseState serves them all, and delegation ONLY emits — reactions belong to the event map. Joining never registers an element as a result.

<button data-kismet-pin data-kismet-slug="fern-ridge-cabin">…</button>
<article data-kismet-card data-kismet-slug="pacific-coast-house">…</article>
<img data-kismet-hover data-kismet-slug="deschutes-river-lodge" … />
  • resultVacationRentals — the vacation rentals on the map, grid, and rail. Sort and filter apply to these.
  • openableVacationRentals — anything that can open the sheet, such as related vacation rentals or review-strip vacation rentals.

An openable-only vacation rental (in openableVacationRentals but not resultVacationRentals) can open the quick view but never gets a pin or a grid slot.

map-view builds on the browse channel. Its MUSTs are the map’s own facts: each [data-kismet-pin] emits Map Pin Clicked with its slug (conformance/browse-pin-emits, captured by the conformance runner through the __KISMET_EVENTS__ test sink); vacation rentals at the same point (equal to 4 decimal places) form one “N vacation rentals · from $X” building pin whose click opens a chooser, while a single-vacation rental building opens the sheet directly; zoom clusters stay separate from building pins; dated pins demote vacation rentals unavailable for the active stay; the initial view fits the main cluster, ignoring outliers; with no maps key the map still shows pins, just no tiles; and pins are buttons with name + price, reachable by keyboard. Reactions — a pin click opening the quick view and card↔pin hover both ways — are SHOULDs under the default event map (warning only, never blocking). “Search this area” is opt-in (SHOULD). The pin math and the map component are not in the SDK core — they are the KismetMap work (Linear KIS-17820, KIS-17837).