Between tool calls, the model is blind: it doesn’t watch the user’s interactions with the UI. Yet, to answer the user’s next message, the model often needs to know what’s on screen. State is how the view closes that gap, and the design decision is always the same: what should the model see, what shouldn’t, and when.Here’s a complete view, the carousel mounted by a tool call to search-products, holding shared state, hidden state, and a narration for the model:
views/carousel.tsx
import { useState } from "react";import { useViewState } from "skybridge/web";import { useCallTool, useToolInfo } from "../helpers.js"; // generated, type-safe from server schemaexport default function Carousel() { const { output, responseMetadata } = useToolInfo<"search-products">(); const { callTool: checkout } = useCallTool("create-checkout"); const [cart, setCart] = useViewState<{ ids: string[] }>({ ids: [] }); // shared with the model, survives remounts const [hovered, setHovered] = useState<string | null>(null); // invisible to the model, gone on remount return ( <div data-llm={cart.ids.length ? `Cart holds ${cart.ids.length} items` : "Cart is empty"}> <ProductGrid products={output.products} images={responseMetadata.images} cart={cart.ids} highlighted={hovered} onHover={setHovered} onAdd={(id) => setCart({ ids: [...cart.ids, id] })} onCheckout={() => checkout({ productIds: cart.ids })} /> </div> );}
The sections below cover the decision itself, sharing and persisting state with useViewState, and narrating the screen with data-llm.
The model doesn’t watch the view live: it reads state when the conversation comes back to it, meaning on the next user message. Models can’t write the state directly. Each view instance owns its state; models typically see one instance per view, whichever had its state most recently updated.
ChatGPT pushes state updates to the model context depending on the display mode: updates from a PiP or fullscreen view are always pushed; updates from an inline view are pushed only while the instance has just mounted and no conversation turn has happened since.
cart is shared because the conversation needs it: the model reads it to recommend a matching item or answer “what’s in my cart?”. hovered stays in plain useState because a hover ends before the user’s next message: by the time the model could read it, the value is stale.
While useViewState pushes structured data to the model context, data-llm describes what the user currently sees in natural language. It’s recomputed on every render and synced to the model:
This is what lets the user speak in references: “what do you think of this one?”. The model resolves this one from the data-llm narration. Nest data-llm on inner elements and the model receives an indented outline of the screen.
All done!
You now know how to manage state. Learn who’s behind every tool call in the next chapter.