@noodleseed/agent-kit 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/manifest.json +545 -5
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/examples/acme-bistro/README.md +51 -0
  5. package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +435 -0
  6. package/skills/claude-code/examples/acme-bistro/design/api-contract.md +161 -0
  7. package/skills/claude-code/examples/acme-bistro/design/wireframe.html +573 -0
  8. package/skills/claude-code/examples/acme-bistro/noodle.json +5 -0
  9. package/skills/claude-code/examples/acme-bistro/package.json +20 -0
  10. package/skills/claude-code/examples/acme-bistro/src/helpers.ts +7 -0
  11. package/skills/claude-code/examples/acme-bistro/src/server.ts +149 -0
  12. package/skills/claude-code/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
  13. package/skills/claude-code/examples/acme-bistro/src/views/widget-style.css +228 -0
  14. package/skills/claude-code/examples/acme-bistro/test/server.test.ts +21 -0
  15. package/skills/claude-code/examples/acme-bistro/vitest.config.ts +7 -0
  16. package/skills/claude-code/examples/acme-discovery/README.md +85 -0
  17. package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +441 -0
  18. package/skills/claude-code/examples/acme-discovery/design/wireframe.html +670 -0
  19. package/skills/claude-code/examples/acme-discovery/noodle.json +5 -0
  20. package/skills/claude-code/examples/acme-discovery/package.json +20 -0
  21. package/skills/claude-code/examples/acme-discovery/src/helpers.ts +7 -0
  22. package/skills/claude-code/examples/acme-discovery/src/server.ts +202 -0
  23. package/skills/claude-code/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
  24. package/skills/claude-code/examples/acme-discovery/src/views/widget-style.css +207 -0
  25. package/skills/claude-code/examples/acme-discovery/test/server.test.ts +24 -0
  26. package/skills/claude-code/examples/acme-discovery/vitest.config.ts +7 -0
  27. package/skills/claude-code/examples/acme-tasks/README.md +80 -0
  28. package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +277 -0
  29. package/skills/claude-code/examples/acme-tasks/design/wireframe.html +603 -0
  30. package/skills/claude-code/examples/acme-tasks/noodle.json +5 -0
  31. package/skills/claude-code/examples/acme-tasks/package.json +20 -0
  32. package/skills/claude-code/examples/acme-tasks/src/helpers.ts +6 -0
  33. package/skills/claude-code/examples/acme-tasks/src/server.ts +141 -0
  34. package/skills/claude-code/examples/acme-tasks/src/views/task-list.tsx +177 -0
  35. package/skills/claude-code/examples/acme-tasks/src/views/widget-style.css +227 -0
  36. package/skills/claude-code/examples/acme-tasks/test/server.test.ts +22 -0
  37. package/skills/claude-code/examples/acme-tasks/vitest.config.ts +7 -0
  38. package/skills/claude-code/examples/food-ordering/README.md +82 -0
  39. package/skills/claude-code/examples/food-ordering/noodle.json +5 -0
  40. package/skills/claude-code/examples/food-ordering/package.json +22 -0
  41. package/skills/claude-code/examples/food-ordering/src/helpers.ts +34 -0
  42. package/skills/claude-code/examples/food-ordering/src/server.ts +412 -0
  43. package/skills/claude-code/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
  44. package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
  45. package/skills/claude-code/examples/food-ordering/src/views/widget-style.css +682 -0
  46. package/skills/claude-code/examples/food-ordering/test/server.test.ts +58 -0
  47. package/skills/claude-code/examples/food-ordering/vitest.config.ts +30 -0
  48. package/skills/claude-code/examples/hello/README.md +13 -0
  49. package/skills/claude-code/examples/hello/noodle.json +5 -0
  50. package/skills/claude-code/examples/hello/package.json +16 -0
  51. package/skills/claude-code/examples/hello/src/server.ts +29 -0
  52. package/skills/claude-code/examples/hello/test/server.test.ts +8 -0
  53. package/skills/claude-code/examples/weather/README.md +54 -0
  54. package/skills/claude-code/examples/weather/noodle.json +4 -0
  55. package/skills/claude-code/examples/weather/package.json +16 -0
  56. package/skills/claude-code/examples/weather/src/server.ts +190 -0
  57. package/skills/claude-code/examples/weather/test/server.test.ts +8 -0
  58. package/skills/claude-code/references/examples.md +16 -7
  59. package/skills/codex/SKILL.md +1 -1
  60. package/skills/codex/examples/acme-bistro/README.md +51 -0
  61. package/skills/codex/examples/acme-bistro/design/UX-Document.md +435 -0
  62. package/skills/codex/examples/acme-bistro/design/api-contract.md +161 -0
  63. package/skills/codex/examples/acme-bistro/design/wireframe.html +573 -0
  64. package/skills/codex/examples/acme-bistro/noodle.json +5 -0
  65. package/skills/codex/examples/acme-bistro/package.json +20 -0
  66. package/skills/codex/examples/acme-bistro/src/helpers.ts +7 -0
  67. package/skills/codex/examples/acme-bistro/src/server.ts +149 -0
  68. package/skills/codex/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
  69. package/skills/codex/examples/acme-bistro/src/views/widget-style.css +228 -0
  70. package/skills/codex/examples/acme-bistro/test/server.test.ts +21 -0
  71. package/skills/codex/examples/acme-bistro/vitest.config.ts +7 -0
  72. package/skills/codex/examples/acme-discovery/README.md +85 -0
  73. package/skills/codex/examples/acme-discovery/design/UX-Document.md +441 -0
  74. package/skills/codex/examples/acme-discovery/design/wireframe.html +670 -0
  75. package/skills/codex/examples/acme-discovery/noodle.json +5 -0
  76. package/skills/codex/examples/acme-discovery/package.json +20 -0
  77. package/skills/codex/examples/acme-discovery/src/helpers.ts +7 -0
  78. package/skills/codex/examples/acme-discovery/src/server.ts +202 -0
  79. package/skills/codex/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
  80. package/skills/codex/examples/acme-discovery/src/views/widget-style.css +207 -0
  81. package/skills/codex/examples/acme-discovery/test/server.test.ts +24 -0
  82. package/skills/codex/examples/acme-discovery/vitest.config.ts +7 -0
  83. package/skills/codex/examples/acme-tasks/README.md +80 -0
  84. package/skills/codex/examples/acme-tasks/design/UX-Document.md +277 -0
  85. package/skills/codex/examples/acme-tasks/design/wireframe.html +603 -0
  86. package/skills/codex/examples/acme-tasks/noodle.json +5 -0
  87. package/skills/codex/examples/acme-tasks/package.json +20 -0
  88. package/skills/codex/examples/acme-tasks/src/helpers.ts +6 -0
  89. package/skills/codex/examples/acme-tasks/src/server.ts +141 -0
  90. package/skills/codex/examples/acme-tasks/src/views/task-list.tsx +177 -0
  91. package/skills/codex/examples/acme-tasks/src/views/widget-style.css +227 -0
  92. package/skills/codex/examples/acme-tasks/test/server.test.ts +22 -0
  93. package/skills/codex/examples/acme-tasks/vitest.config.ts +7 -0
  94. package/skills/codex/examples/food-ordering/README.md +82 -0
  95. package/skills/codex/examples/food-ordering/noodle.json +5 -0
  96. package/skills/codex/examples/food-ordering/package.json +22 -0
  97. package/skills/codex/examples/food-ordering/src/helpers.ts +34 -0
  98. package/skills/codex/examples/food-ordering/src/server.ts +412 -0
  99. package/skills/codex/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
  100. package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
  101. package/skills/codex/examples/food-ordering/src/views/widget-style.css +682 -0
  102. package/skills/codex/examples/food-ordering/test/server.test.ts +58 -0
  103. package/skills/codex/examples/food-ordering/vitest.config.ts +30 -0
  104. package/skills/codex/examples/hello/README.md +13 -0
  105. package/skills/codex/examples/hello/noodle.json +5 -0
  106. package/skills/codex/examples/hello/package.json +16 -0
  107. package/skills/codex/examples/hello/src/server.ts +29 -0
  108. package/skills/codex/examples/hello/test/server.test.ts +8 -0
  109. package/skills/codex/examples/weather/README.md +54 -0
  110. package/skills/codex/examples/weather/noodle.json +4 -0
  111. package/skills/codex/examples/weather/package.json +16 -0
  112. package/skills/codex/examples/weather/src/server.ts +190 -0
  113. package/skills/codex/examples/weather/test/server.test.ts +8 -0
  114. package/skills/codex/references/examples.md +16 -7
@@ -0,0 +1,659 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ ActionBar,
4
+ AppShell,
5
+ AsyncBoundary,
6
+ ChoiceGroup,
7
+ createViewStore,
8
+ DataCard,
9
+ DataList,
10
+ Field,
11
+ HandoffButton,
12
+ QuantityStepper,
13
+ ShellNav,
14
+ StatusBadge,
15
+ SubmitButton,
16
+ useAppFlow,
17
+ useCallTool,
18
+ useHandoff,
19
+ useLayout,
20
+ useSendFollowUpMessage,
21
+ useToolInfo,
22
+ useViewState,
23
+ View,
24
+ ViewStack,
25
+ } from '../helpers.js';
26
+ import './widget-style.css';
27
+
28
+ type ViewName = 'stores' | 'menu' | 'item' | 'cart' | 'review' | 'handoff';
29
+
30
+ type Store = {
31
+ readonly id: string;
32
+ readonly name: string;
33
+ readonly cuisine: string;
34
+ readonly address: string;
35
+ readonly open: boolean;
36
+ readonly etaMinutes: number;
37
+ readonly rating: number;
38
+ };
39
+
40
+ type MenuItem = {
41
+ readonly id: string;
42
+ readonly storeId: string;
43
+ readonly category: string;
44
+ readonly name: string;
45
+ readonly price: number;
46
+ readonly description: string;
47
+ readonly modifiers: readonly string[];
48
+ };
49
+
50
+ type CartLine = {
51
+ readonly itemId: string;
52
+ readonly quantity: number;
53
+ readonly modifiers: readonly string[];
54
+ readonly note?: string;
55
+ };
56
+
57
+ type CartState = {
58
+ readonly selectedStoreId?: string;
59
+ readonly lines: readonly CartLine[];
60
+ readonly customer: string;
61
+ readonly notes?: string;
62
+ readonly subtotal: number;
63
+ readonly status: 'draft' | 'review' | 'handoff';
64
+ readonly checkoutUrl?: string;
65
+ };
66
+
67
+ const zeroCart: CartState = {
68
+ lines: [],
69
+ customer: 'Guest',
70
+ subtotal: 0,
71
+ status: 'draft',
72
+ };
73
+
74
+ const useCartStore = createViewStore<CartState>('cart', zeroCart);
75
+ const useCartRevisionStore = createViewStore('cart_revision', { value: 0 });
76
+
77
+ function structured<T>(value: unknown): T | undefined {
78
+ return (value as { structuredContent?: T } | undefined)?.structuredContent;
79
+ }
80
+
81
+ function currency(value: number): string {
82
+ return `$${value.toFixed(2)}`;
83
+ }
84
+
85
+ function modifierLabel(value: string): string {
86
+ return value
87
+ .split('_')
88
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
89
+ .join(' ');
90
+ }
91
+
92
+ export default function OrderingFlow() {
93
+ const { displayMode, theme } = useLayout();
94
+ const entry = structured<{
95
+ readonly customer?: string;
96
+ readonly stores?: readonly Store[];
97
+ readonly featuredItems?: readonly MenuItem[];
98
+ readonly status?: string;
99
+ }>(useToolInfo('open_ordering'));
100
+ const flow = useAppFlow<ViewName>({
101
+ key: 'ordering_flow',
102
+ initialView: 'stores',
103
+ views: ['stores', 'menu', 'item', 'cart', 'review', 'handoff'],
104
+ });
105
+ const view = flow.activeView;
106
+ const setView = flow.navigate;
107
+ const handoff = useHandoff();
108
+ const sendFollowUpMessage = useSendFollowUpMessage();
109
+ const searchStores = useCallTool('search_stores');
110
+ const loadMenu = useCallTool('load_menu');
111
+ const loadItem = useCallTool('load_item');
112
+ const readCart = useCallTool('read_cart');
113
+ const syncCart = useCallTool('sync_cart');
114
+ const prepareCheckout = useCallTool('prepare_checkout');
115
+
116
+ const [query, setQuery] = useViewState('query', '');
117
+ const [customer, setCustomer] = useViewState('customer', entry?.customer ?? 'Guest');
118
+ const [selectedStoreId, setSelectedStoreId] = useViewState<string | undefined>(
119
+ 'selected_store',
120
+ entry?.stores?.[0]?.id,
121
+ );
122
+ const [selectedItemId, setSelectedItemId] = useViewState<string | undefined>(
123
+ 'selected_item',
124
+ entry?.featuredItems?.[0]?.id,
125
+ );
126
+ const cartStore = useCartStore();
127
+ const revisionStore = useCartRevisionStore((state) => state.value);
128
+ const cart =
129
+ cartStore.state.customer === 'Guest' && entry?.customer
130
+ ? { ...cartStore.state, customer: entry.customer }
131
+ : cartStore.state;
132
+ const revision = revisionStore.selected;
133
+ const setCart = cartStore.setState;
134
+ const setRevision = (value: number) => revisionStore.setState({ value });
135
+ const [quantity, setQuantity] = useState(1);
136
+ const [selectedModifiers, setSelectedModifiers] = useState<readonly string[]>([]);
137
+
138
+ const storeData = structured<{ readonly stores?: readonly Store[] }>(searchStores.data);
139
+ const menuData = structured<{
140
+ readonly stores?: readonly Store[];
141
+ readonly items?: readonly MenuItem[];
142
+ }>(loadMenu.data);
143
+ const itemData = structured<{ readonly items?: readonly MenuItem[] }>(loadItem.data);
144
+ const checkoutData = structured<{ readonly checkoutUrl?: string; readonly cart?: CartState }>(
145
+ prepareCheckout.data,
146
+ );
147
+ const stores = storeData?.stores ?? entry?.stores ?? [];
148
+ const queryText = query.trim().toLowerCase();
149
+ const displayedStores = stores.filter(
150
+ (store) =>
151
+ queryText.length === 0 ||
152
+ store.name.toLowerCase().includes(queryText) ||
153
+ store.cuisine.toLowerCase().includes(queryText),
154
+ );
155
+ const allItems = menuData?.items ?? itemData?.items ?? entry?.featuredItems ?? [];
156
+ const items = allItems.filter((item) => item.storeId === selectedStoreId);
157
+ const currentStore =
158
+ stores.find((store) => store.id === selectedStoreId) ?? menuData?.stores?.[0] ?? stores[0];
159
+ const currentItem = allItems.find((item) => item.id === selectedItemId) ?? items[0];
160
+ const lineItems = useMemo(
161
+ () =>
162
+ cart.lines.map((line) => ({
163
+ ...line,
164
+ item: [...(entry?.featuredItems ?? []), ...allItems].find(
165
+ (item) => item.id === line.itemId,
166
+ ),
167
+ })),
168
+ [cart.lines, entry?.featuredItems, allItems],
169
+ );
170
+ const llmSummary = `${view} view for ${customer}; ${cart.lines.length} cart lines; subtotal ${currency(
171
+ cart.subtotal,
172
+ )}`;
173
+
174
+ async function chooseStore(store: Store) {
175
+ setSelectedStoreId(store.id);
176
+ setView('menu');
177
+ const result = await loadMenu.callTool({ storeId: store.id });
178
+ const loaded = structured<{ readonly items?: readonly MenuItem[] }>(result);
179
+ setSelectedItemId(loaded?.items?.find((item) => item.storeId === store.id)?.id);
180
+ }
181
+
182
+ async function chooseItem(item: MenuItem) {
183
+ setSelectedItemId(item.id);
184
+ setSelectedModifiers([]);
185
+ setQuantity(1);
186
+ setView('item');
187
+ await loadItem.callTool({ itemId: item.id });
188
+ }
189
+
190
+ async function persistCart(nextCart: CartState, nextView: ViewName) {
191
+ const result = await syncCart.callTool({
192
+ selectedStoreId: nextCart.selectedStoreId,
193
+ lines: nextCart.lines,
194
+ customer: nextCart.customer,
195
+ notes: nextCart.notes,
196
+ subtotal: nextCart.subtotal,
197
+ expectedRevision: revision,
198
+ });
199
+ const synced = structured<{ readonly cart?: CartState; readonly revision?: number }>(result);
200
+ setCart(synced?.cart ?? nextCart);
201
+ setRevision(synced?.revision ?? revision + 1);
202
+ setView(nextView);
203
+ }
204
+
205
+ async function addCurrentItem() {
206
+ if (!currentItem) return;
207
+ const nextLines = [
208
+ ...cart.lines,
209
+ {
210
+ itemId: currentItem.id,
211
+ quantity,
212
+ modifiers: selectedModifiers,
213
+ },
214
+ ];
215
+ await persistCart(
216
+ {
217
+ ...cart,
218
+ selectedStoreId: currentStore?.id ?? selectedStoreId,
219
+ lines: nextLines,
220
+ customer,
221
+ subtotal: nextLines.reduce(
222
+ (sum, line) =>
223
+ sum +
224
+ ([...(entry?.featuredItems ?? []), ...allItems].find((item) => item.id === line.itemId)
225
+ ?.price ?? 0) *
226
+ line.quantity,
227
+ 0,
228
+ ),
229
+ status: 'draft',
230
+ },
231
+ 'cart',
232
+ );
233
+ }
234
+
235
+ async function prepareHandoff() {
236
+ const result = await prepareCheckout.callTool({
237
+ selectedStoreId: cart.selectedStoreId,
238
+ lines: cart.lines,
239
+ customer,
240
+ notes: cart.notes,
241
+ subtotal: cart.subtotal,
242
+ expectedRevision: revision,
243
+ });
244
+ const prepared = structured<{
245
+ readonly cart?: CartState;
246
+ readonly revision?: number;
247
+ readonly checkoutUrl?: string;
248
+ }>(result);
249
+ setCart(prepared?.cart ?? cart);
250
+ setRevision(prepared?.revision ?? revision + 1);
251
+ setView('handoff');
252
+ }
253
+
254
+ return (
255
+ <AppShell
256
+ className={`nw-shell${theme === 'dark' ? ' dark' : ''}`}
257
+ data-llm={llmSummary}
258
+ title="Food Ordering"
259
+ subtitle={entry?.status ?? 'Find a store, build a cart, and hand off checkout.'}
260
+ icon={<BowlIcon />}
261
+ badge={displayMode === 'fullscreen' ? 'Fullscreen' : view}
262
+ footer={
263
+ <>
264
+ <span className="nw-meta">
265
+ <BagIcon />
266
+ {cart.lines.length} items
267
+ </span>
268
+ <span className="nw-meta">Revision {revision}</span>
269
+ </>
270
+ }
271
+ >
272
+ <ShellNav
273
+ activeView={view}
274
+ aria-label="Ordering steps"
275
+ items={(['stores', 'menu', 'cart', 'review', 'handoff'] as const).map((step) => ({
276
+ view: step,
277
+ label: step,
278
+ }))}
279
+ onNavigate={setView}
280
+ />
281
+ <div className="nw-body">
282
+ <ViewStack flow={flow}>
283
+ <View name="stores">
284
+ <div className="nw-field-grid">
285
+ <Field className="nw-field" label="Customer">
286
+ <input
287
+ className="nw-input"
288
+ value={customer}
289
+ onChange={(event) => setCustomer(event.currentTarget.value)}
290
+ />
291
+ </Field>
292
+ <Field className="nw-field" label="Search">
293
+ <input
294
+ className="nw-input"
295
+ value={query}
296
+ placeholder="Noodles"
297
+ onChange={(event) => setQuery(event.currentTarget.value)}
298
+ />
299
+ </Field>
300
+ </div>
301
+ <ActionBar className="nw-actions">
302
+ <SubmitButton
303
+ className="nw-button nw-button-primary"
304
+ pending={searchStores.isPending}
305
+ pendingLabel="Searching..."
306
+ onClick={() => searchStores.callTool({ query, openOnly: false })}
307
+ >
308
+ <SearchIcon />
309
+ Search stores
310
+ </SubmitButton>
311
+ <SubmitButton
312
+ className="nw-button"
313
+ pending={readCart.isPending}
314
+ pendingLabel="Loading..."
315
+ onClick={async () => {
316
+ const result = await readCart.callTool({});
317
+ const stored = structured<{
318
+ readonly value?: CartState;
319
+ readonly revision?: number;
320
+ }>(result);
321
+ if (stored?.value?.lines) setCart(stored.value);
322
+ setRevision(stored?.revision ?? revision);
323
+ }}
324
+ >
325
+ <RefreshIcon />
326
+ Load cart
327
+ </SubmitButton>
328
+ </ActionBar>
329
+ <AsyncBoundary
330
+ state={searchStores}
331
+ isEmpty={displayedStores.length === 0}
332
+ empty="No matching stores"
333
+ >
334
+ <StoreList stores={displayedStores} onChoose={chooseStore} />
335
+ </AsyncBoundary>
336
+ </View>
337
+
338
+ <View name="menu">
339
+ <SectionHeader title={currentStore?.name ?? 'Menu'} detail={currentStore?.address} />
340
+ <AsyncBoundary
341
+ state={loadMenu}
342
+ isEmpty={items.length === 0}
343
+ empty="No menu items loaded"
344
+ >
345
+ <ItemList items={items} onChoose={chooseItem} />
346
+ </AsyncBoundary>
347
+ </View>
348
+
349
+ <View name="item">
350
+ {currentItem ? (
351
+ <>
352
+ <SectionHeader title={currentItem.name} detail={currentItem.description} />
353
+ <div className="nw-field-grid">
354
+ <Field className="nw-field" label="Quantity">
355
+ <QuantityStepper value={quantity} min={1} onChange={setQuantity} />
356
+ </Field>
357
+ <div className="nw-summary nw-summary-compact">
358
+ <div className="nw-summary-row">
359
+ <dt>Price</dt>
360
+ <dd>{currency(currentItem.price)}</dd>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ <ChoiceGroup
365
+ className="nw-modifier-grid"
366
+ values={currentItem.modifiers}
367
+ selected={selectedModifiers}
368
+ onChange={setSelectedModifiers}
369
+ labelFor={modifierLabel}
370
+ />
371
+ <ActionBar className="nw-actions">
372
+ <SubmitButton
373
+ className="nw-button nw-button-primary"
374
+ pending={syncCart.isPending}
375
+ pendingLabel="Adding..."
376
+ onClick={addCurrentItem}
377
+ >
378
+ <BagIcon />
379
+ Add to cart
380
+ </SubmitButton>
381
+ <button className="nw-button" type="button" onClick={() => setView('menu')}>
382
+ Back to menu
383
+ </button>
384
+ </ActionBar>
385
+ </>
386
+ ) : null}
387
+ </View>
388
+
389
+ <View name="cart">
390
+ <SectionHeader
391
+ title="Cart"
392
+ detail={`${cart.lines.length} line${cart.lines.length === 1 ? '' : 's'}`}
393
+ />
394
+ <CartSummary lineItems={lineItems} subtotal={cart.subtotal} />
395
+ <Field className="nw-field" label="Notes">
396
+ <input
397
+ className="nw-input"
398
+ value={cart.notes ?? ''}
399
+ placeholder="Utensils, pickup name, or allergies"
400
+ onChange={(event) => setCart({ ...cart, notes: event.currentTarget.value })}
401
+ />
402
+ </Field>
403
+ <ActionBar className="nw-actions">
404
+ <SubmitButton
405
+ className="nw-button"
406
+ pending={syncCart.isPending}
407
+ pendingLabel="Saving..."
408
+ onClick={() => persistCart({ ...cart, customer, status: 'draft' }, 'review')}
409
+ >
410
+ <SaveIcon />
411
+ Save cart
412
+ </SubmitButton>
413
+ <SubmitButton
414
+ className="nw-button nw-button-primary"
415
+ disabled={cart.lines.length === 0}
416
+ pending={prepareCheckout.isPending}
417
+ pendingLabel="Preparing..."
418
+ onClick={prepareHandoff}
419
+ >
420
+ <ExternalIcon />
421
+ Prepare checkout
422
+ </SubmitButton>
423
+ </ActionBar>
424
+ </View>
425
+
426
+ <View name="review">
427
+ <SectionHeader
428
+ title="Review order"
429
+ detail={`${cart.lines.length} line${cart.lines.length === 1 ? '' : 's'}`}
430
+ />
431
+ <CartSummary lineItems={lineItems} subtotal={cart.subtotal} />
432
+ <ActionBar className="nw-actions">
433
+ <button className="nw-button" type="button" onClick={() => setView('cart')}>
434
+ Edit cart
435
+ </button>
436
+ <SubmitButton
437
+ className="nw-button nw-button-primary"
438
+ disabled={cart.lines.length === 0}
439
+ pending={prepareCheckout.isPending}
440
+ pendingLabel="Preparing..."
441
+ onClick={prepareHandoff}
442
+ >
443
+ <ExternalIcon />
444
+ Prepare checkout
445
+ </SubmitButton>
446
+ </ActionBar>
447
+ </View>
448
+
449
+ <View name="handoff">
450
+ <SectionHeader
451
+ title="Checkout handoff"
452
+ detail="Payment stays with the ordering site."
453
+ />
454
+ <CartSummary lineItems={lineItems} subtotal={cart.subtotal} />
455
+ <p className="nw-note">
456
+ The MCP App prepared a synthetic checkout URL and will open only the allowlisted
457
+ example domain.
458
+ </p>
459
+ <ActionBar className="nw-actions">
460
+ <HandoffButton
461
+ handoff={handoff}
462
+ target={checkoutData?.checkoutUrl ?? cart.checkoutUrl ?? ''}
463
+ className="nw-button nw-button-primary"
464
+ type="button"
465
+ pendingLabel="Opening..."
466
+ >
467
+ <ExternalIcon />
468
+ Continue checkout
469
+ </HandoffButton>
470
+ <button
471
+ className="nw-button"
472
+ type="button"
473
+ onClick={() =>
474
+ sendFollowUpMessage({
475
+ prompt: `Summarize my food order for ${customer} before checkout.`,
476
+ })
477
+ }
478
+ >
479
+ <SparkIcon />
480
+ Ask assistant
481
+ </button>
482
+ </ActionBar>
483
+ </View>
484
+ </ViewStack>
485
+ </div>
486
+ </AppShell>
487
+ );
488
+ }
489
+
490
+ function SectionHeader({ title, detail }: { readonly title: string; readonly detail?: string }) {
491
+ return (
492
+ <div className="nw-section-head">
493
+ <p className="nw-section-title">{title}</p>
494
+ {detail ? <p className="nw-section-detail">{detail}</p> : null}
495
+ </div>
496
+ );
497
+ }
498
+
499
+ function StoreList({
500
+ stores,
501
+ onChoose,
502
+ }: {
503
+ readonly stores: readonly Store[];
504
+ readonly onChoose: (store: Store) => void;
505
+ }) {
506
+ return (
507
+ <DataList className="nw-menu-list">
508
+ {stores.map((store) => (
509
+ <DataCard
510
+ as="button"
511
+ className="nw-menu-item"
512
+ key={store.id}
513
+ type="button"
514
+ onClick={() => onChoose(store)}
515
+ >
516
+ <span>
517
+ <span className="nw-menu-name">{store.name}</span>
518
+ <span className="nw-menu-desc">
519
+ {store.cuisine} · {store.address}
520
+ </span>
521
+ </span>
522
+ <StatusBadge className="nw-status" tone={store.open ? 'success' : 'neutral'}>
523
+ {store.open ? `${store.etaMinutes}m` : 'Closed'}
524
+ </StatusBadge>
525
+ </DataCard>
526
+ ))}
527
+ </DataList>
528
+ );
529
+ }
530
+
531
+ function ItemList({
532
+ items,
533
+ onChoose,
534
+ }: {
535
+ readonly items: readonly MenuItem[];
536
+ readonly onChoose: (item: MenuItem) => void;
537
+ }) {
538
+ return (
539
+ <DataList className="nw-menu-list">
540
+ {items.map((item) => (
541
+ <DataCard
542
+ as="button"
543
+ className="nw-menu-item"
544
+ key={item.id}
545
+ type="button"
546
+ onClick={() => onChoose(item)}
547
+ >
548
+ <span>
549
+ <span className="nw-menu-name">{item.name}</span>
550
+ <span className="nw-menu-desc">{item.description}</span>
551
+ </span>
552
+ <span className="nw-price">{currency(item.price)}</span>
553
+ </DataCard>
554
+ ))}
555
+ </DataList>
556
+ );
557
+ }
558
+
559
+ function CartSummary({
560
+ lineItems,
561
+ subtotal,
562
+ }: {
563
+ readonly lineItems: readonly (CartLine & { readonly item?: MenuItem })[];
564
+ readonly subtotal: number;
565
+ }) {
566
+ return (
567
+ <dl className="nw-summary">
568
+ {lineItems.length === 0 ? (
569
+ <div className="nw-summary-row">
570
+ <dt>No items yet</dt>
571
+ <dd>{currency(0)}</dd>
572
+ </div>
573
+ ) : (
574
+ lineItems.map((line, index) => (
575
+ <div className="nw-summary-row" key={`${line.itemId}-${index}`}>
576
+ <dt>
577
+ {line.quantity} x {line.item?.name ?? line.itemId}
578
+ {line.modifiers.length > 0 ? (
579
+ <span className="nw-line-note">{line.modifiers.map(modifierLabel).join(', ')}</span>
580
+ ) : null}
581
+ </dt>
582
+ <dd>{currency((line.item?.price ?? 0) * line.quantity)}</dd>
583
+ </div>
584
+ ))
585
+ )}
586
+ <div className="nw-summary-row nw-total">
587
+ <dt>Subtotal</dt>
588
+ <dd>{currency(subtotal)}</dd>
589
+ </div>
590
+ </dl>
591
+ );
592
+ }
593
+
594
+ function BowlIcon() {
595
+ return (
596
+ <svg viewBox="0 0 24 24" aria-hidden="true">
597
+ <path d="M4 11h16a7 7 0 0 1-7 7h-2a7 7 0 0 1-7-7Z" />
598
+ <path d="M7 21h10" />
599
+ <path d="M8 6c0-1 1-1 1-2" />
600
+ <path d="M12 6c0-1 1-1 1-2" />
601
+ <path d="M16 6c0-1 1-1 1-2" />
602
+ </svg>
603
+ );
604
+ }
605
+
606
+ function SearchIcon() {
607
+ return (
608
+ <svg viewBox="0 0 24 24" aria-hidden="true">
609
+ <circle cx="11" cy="11" r="7" />
610
+ <path d="m20 20-4-4" />
611
+ </svg>
612
+ );
613
+ }
614
+
615
+ function RefreshIcon() {
616
+ return (
617
+ <svg viewBox="0 0 24 24" aria-hidden="true">
618
+ <path d="M20 12a8 8 0 1 1-2.3-5.6" />
619
+ <path d="M20 4v5h-5" />
620
+ </svg>
621
+ );
622
+ }
623
+
624
+ function BagIcon() {
625
+ return (
626
+ <svg viewBox="0 0 24 24" aria-hidden="true">
627
+ <path d="M6 8h12l-1 12H7L6 8Z" />
628
+ <path d="M9 8a3 3 0 0 1 6 0" />
629
+ </svg>
630
+ );
631
+ }
632
+
633
+ function SaveIcon() {
634
+ return (
635
+ <svg viewBox="0 0 24 24" aria-hidden="true">
636
+ <path d="M5 4h12l2 2v14H5V4Z" />
637
+ <path d="M8 4v6h8" />
638
+ <path d="M8 20v-5h8v5" />
639
+ </svg>
640
+ );
641
+ }
642
+
643
+ function ExternalIcon() {
644
+ return (
645
+ <svg viewBox="0 0 24 24" aria-hidden="true">
646
+ <path d="M14 4h6v6" />
647
+ <path d="m20 4-9 9" />
648
+ <path d="M20 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5" />
649
+ </svg>
650
+ );
651
+ }
652
+
653
+ function SparkIcon() {
654
+ return (
655
+ <svg viewBox="0 0 24 24" aria-hidden="true">
656
+ <path d="m12 3 1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3Z" />
657
+ </svg>
658
+ );
659
+ }