The cache is the screen
The assistant sees what the person sees by reading the query cache when a message is sent. Every query names what it fetches, so pages register nothing.
Someone looking at a list asks the assistant “which of these is worst?” The assistant has to know what “these” is. The usual fix is a context object per page, built by hand and handed to the assistant. It drifts. Half the pages never get one, and the ones that do go stale the first time a component changes what it fetches.
But the page already knows what it shows: whatever its mounted components are reading from the query cache. So the screen is read from the cache, not declared.
// every query names what it fetches, in a reader's words
export const openOrdersQuery = (customerId: string) =>
queryOptions({
queryKey: ["orders", customerId, "open"],
queryFn: () => api.orders.list({ customerId, status: "open" }),
description: "Open orders for this customer", // required: the factory won't compile without it
});
// read on every page (the signed-in user, plan features): never "on this page"
queryOptions({ queryKey: ["me"], queryFn: api.me, description: "The signed-in user", meta: { session: true } });
The factory stamps description on the query’s meta. When a message is sent, one function reads the screen:
function readScreen(router: Router, queryClient: QueryClient): ScreenView {
const onScreen = queryClient.getQueryCache().getAll().filter((q) =>
q.meta?.description && !q.meta.session && q.isActive() && q.state.data !== undefined);
return {
path: router.state.location.pathname,
title: viewOf(router.state.matches)?.title ?? "This page",
search: currentSearch(router), // the filters, straight from the URL
content: onScreen.slice(0, 15).map((q) => ({
description: q.meta.description,
...sample(q.state.data), // 20 rows, flattened, each cut to 600 chars
})),
};
}
mounted components ──read──▶ query cache ◀── readScreen(), called at send
│ active · described · not session
▼
POST /assistant { view: { path, title, search, content[] }, messages }
│
the turn's prompt: a bounded copy + "showing 25 of 340 rows"
get-page-context: the full view, when the question is about the page
Rules
- Described or invisible. A query can’t be written without a description, and data that’s on every page is marked
session. What’s left after the filter is the page. - Read at send, not on mount. The snapshot is a getter called when the request is built, so it’s the screen after the last filter, tab switch or page load. It rides the request, not the transcript, so an old turn never carries a stale screen.
- Two budgets. The client samples: rows per list, characters per row, lists per page. The server bounds again for the model, halving rows per list until the view fits, and says what it cut. A list page with hundreds of wide rows once cost ~170k input tokens a turn before this. A read tool returns the full view when the model needs it.
- Not everything is fetched. A value the page holds but didn’t load (an unsaved draft, a proposal under review) is published under a key for as long as the page is mounted:
usePublishMeta("proposal", proposal).
Why it works
Awareness costs nothing per page. A new page is visible to the assistant the day it ships, because writing its queries already described them. And the description is the part the model reads first: “Open orders for this customer” over 20 rows says more than a hand-built context object, and it can’t fall out of date with the component that fetches it.
The page’s title and what it’s for come from its view file. What the person can do on it comes from published actions.
How this fits the larger system: The UI is the engine.