Skip to content

Commit ec7552f

Browse files
authored
fix(agent): repair product grid rendering and prerender error (#482)
* feat(agent): switch assistant model to openai/gpt-5.6-luna * feat(agent): enable shopping assistant * fix(agent): render action bar inside CartProviderWrapper AgentButton mounts AgentPanel (ssr:false), whose CartReconciler calls useCart(). Rendered outside CartProviderWrapper it missed the server fallback and threw "useCart must be used within CartProvider" on hydration. * fix(agent): drop reasoning chunks before pipeJsonRender Reasoning models emit reasoning-start/delta chunks that pass through toUIMessageStream by default; pipeJsonRender only handles text chunks, so reasoning (and any spec JSONL emitted inside it) never became data-spec parts and the UI never rendered. * fix(agent): inline concrete card props instead of repeat/$item bindings The model followed json-render's repeat/$item example, but this app's registry components take plain props and defineRegistry passes element.props through unresolved, so {$item:"price"} reached parsePriceString raw and crashed inside ElementErrorBoundary — which returns null, silently unmounting the whole grid. Instruct the model to emit one /elements entry per card with concrete values (and null compareAtPrice), and make parsePriceString tolerate a non-string so a bad spec degrades instead of throwing. * fix(cart): exclude Hydrogen request-context UUIDs from static shell createShopifyRequestContext calls crypto.randomUUID() for requestGroupId, uniqueToken, and visitToken. During prerender of the static shell (layout's seedCartData and nav's CartIcon) that tripped Cache Components' blocking- prerender-crypto error on every route. await io() before the read so the context is created at request time instead. * feat(agent): return six products per search instead of five Bump the default limit from 5 to 6 in searchProducts, searchCatalog, and browseCollection so grids render an even 3x2/2x3. * fix(agent): return six products from recommendations and searches getProductRecommendations hardcoded slice(0, 5). The model also passed limit: 5 explicitly to the search tools, overriding the schema default of 6, so add a system-prompt rule to omit limit and render every returned product. * chore(agent): disable shopping assistant
1 parent c3a2905 commit ec7552f

11 files changed

Lines changed: 21 additions & 12 deletions

File tree

apps/docs/content/docs/anatomy/agent.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ prerequisites:
66
- /docs/getting-started
77
---
88

9-
The storefront includes an opt-in AI shopping assistant built with [AI SDK](https://ai-sdk.dev). It can search products, browse collections, manage the cart, answer store-policy questions, and render rich product cards through natural conversation. The default model is Google Gemini 3.5 Flash through the Vercel AI Gateway.
9+
The storefront includes an opt-in AI shopping assistant built with [AI SDK](https://ai-sdk.dev). It can search products, browse collections, manage the cart, answer store-policy questions, and render rich product cards through natural conversation. The default model is OpenAI GPT-5.6 Luna through the Vercel AI Gateway.
1010

1111
## How it works
1212

apps/template/app/api/chat/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export async function POST(request: Request) {
109109
pipeJsonRender(
110110
toUIMessageStream({
111111
originalMessages: safeMessages.data,
112+
// pipeJsonRender only understands text deltas; reasoning parts would pass through unhandled.
113+
sendReasoning: false,
112114
stream: result.stream,
113115
tools: agent.tools,
114116
}),

apps/template/app/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
6262
<Footer locale={locale} />
6363
<CartNotifications />
6464
<CartOverlayBridge />
65+
<Suspense>
66+
<ActionBar>{shopConfig.agent.isEnabled && <AgentButton />}</ActionBar>
67+
</Suspense>
6568
</CartProviderWrapper>
66-
<Suspense>
67-
<ActionBar>{shopConfig.agent.isEnabled && <AgentButton />}</ActionBar>
68-
</Suspense>
6969
<Toaster closeButton />
7070
</NextIntlClientProvider>
7171
<Suspense>

apps/template/components/agent/registry.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type { Money } from "@/lib/types";
2020
import { cn } from "@/lib/utils";
2121

2222
function parsePriceString(price: string): Money {
23-
const parts = price.split(" ");
23+
const parts = typeof price === "string" ? price.split(" ") : [];
2424
return {
2525
amount: parts[0] || "0",
2626
currencyCode: parts[1] || "USD",

apps/template/lib/agent/server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ function createSystemPrompt(context: AgentContext): string {
7777
return `${prompt}\n${catalog.prompt({
7878
customRules: [
7979
"When searchProducts, searchCatalog, browseCollection, getProductRecommendations, getProductDetails, or getCatalogProduct returns products successfully, render them with AgentProductCard components. Wrap multiple cards in AgentProductGrid.",
80-
"Pass price and compareAtPrice strings directly from tool results.",
80+
"Never pass a limit to searchProducts, searchCatalog, or browseCollection — omit it so the default of 6 applies. Render every returned product; do not trim the grid to fewer cards.",
81+
"Do not use repeat, $item, $state, $index, or $bindItem. Give each AgentProductCard its own /elements/<key> entry with concrete prop values copied directly from the tool result, and list the card keys in the grid's children array.",
82+
"Pass price and compareAtPrice strings directly from tool results. When a product has no compareAtPrice, use null — never a zero amount.",
8183
"When getCart returns a non-empty cart, render AgentCartSummary using its items, subtotal, total, totalQuantity, and checkoutUrl.",
8284
"After addToCart succeeds, render AgentCartConfirmation using known product context.",
8385
"When multiple variants need a choice, render AgentVariantPicker from getProductDetails and ask the user which variant they want.",
@@ -107,7 +109,7 @@ const tools = {
107109
export function createAgent() {
108110
return new ToolLoopAgent({
109111
instructions: createSystemPrompt(getAgentContext()),
110-
model: "google/gemini-3.5-flash",
112+
model: "openai/gpt-5.6-luna",
111113
stopWhen: isStepCount(10),
112114
tools,
113115
});

apps/template/lib/agent/tools/browse-collection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function browseCollectionTool() {
1010
description: `Browse products in a collection. Get handles from listCollections or the current page context.`,
1111
inputSchema: z.object({
1212
collection: z.string(),
13-
limit: z.number().min(1).max(10).default(5),
13+
limit: z.number().min(1).max(10).default(6),
1414
sortKey: z
1515
.enum(["best-matches", "price-low-to-high", "price-high-to-low", "BEST_SELLING", "CREATED"])
1616
.default("best-matches"),

apps/template/lib/agent/tools/get-recommendations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function getRecommendationsTool() {
2323
return true;
2424
});
2525
return {
26-
products: products.slice(0, 5).map((product) => ({
26+
products: products.slice(0, 6).map((product) => ({
2727
available: product.availableForSale,
2828
handle: product.handle,
2929
image: product.featuredImage?.url ?? null,

apps/template/lib/agent/tools/search-catalog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function searchCatalogTool() {
1515
description: `Search Shopify's native catalog semantically. Prefer this for vague, descriptive, or preference-driven requests.`,
1616
inputSchema: z.object({
1717
intent: z.string().optional(),
18-
limit: z.number().min(1).max(10).default(5),
18+
limit: z.number().min(1).max(10).default(6),
1919
query: z.string(),
2020
}),
2121
execute: async ({ intent, limit, query }) => {

apps/template/lib/agent/tools/search-products.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function searchProductsTool() {
99
return tool({
1010
description: `Search for products by keyword. Use this for exact product lookups or price-sorted searches.`,
1111
inputSchema: z.object({
12-
limit: z.number().min(1).max(10).default(5),
12+
limit: z.number().min(1).max(10).default(6),
1313
query: z.string(),
1414
sortKey: z
1515
.enum(["best-matches", "price-low-to-high", "price-high-to-low"])

apps/template/lib/cart/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
gql,
66
type I18nConfig,
77
} from "@shopify/hydrogen";
8-
import { revalidateTag, updateTag } from "next/cache";
8+
import { io, revalidateTag, updateTag } from "next/cache";
99
import { cookies, headers } from "next/headers";
1010

1111
import { defaultLocale, getCountryCode, getLanguageCode } from "@/lib/i18n";
@@ -72,6 +72,8 @@ export function buildCartIdSetCookieHeader(id: string): string {
7272
/** Starts the full-cart read from the request cookie without awaiting it. */
7373
export function seedCartData() {
7474
return (async () => {
75+
// Hydrogen's createShopifyRequestContext calls crypto.randomUUID(); exclude it from the static shell.
76+
await io();
7577
const i18n = {
7678
country: getCountryCode(defaultLocale),
7779
language: getLanguageCode(defaultLocale),

0 commit comments

Comments
 (0)