@noodleseed/agent-kit 0.44.0 → 0.45.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.
- package/manifest.json +279 -247
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/claude-code/authoring-mcp-servers/SKILL.md +1 -1
- package/skills/claude-code/building-mcp-apps/SKILL.md +1 -1
- package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
- package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
- package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
- package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
- package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
- package/skills/claude-code/examples/customer-auth/README.md +12 -5
- package/skills/claude-code/examples/food-ordering/README.md +1 -0
- package/skills/claude-code/examples/food-ordering/package.json +1 -1
- package/skills/claude-code/examples/food-ordering/src/helpers.ts +1 -0
- package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +31 -27
- package/skills/claude-code/examples/food-ordering/src/views/ordering-result.ts +84 -0
- package/skills/claude-code/examples/food-ordering/test/ordering-flow.test.ts +187 -0
- package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
- package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
- package/skills/claude-code/references/embedded-assistant.md +10 -5
- package/skills/claude-code/references/widgets-and-apps.md +30 -10
- package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
- package/skills/codex/SKILL.md +1 -1
- package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
- package/skills/codex/building-mcp-apps/SKILL.md +1 -1
- package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
- package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
- package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
- package/skills/codex/designing-mcp-products/SKILL.md +1 -1
- package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
- package/skills/codex/examples/customer-auth/README.md +12 -5
- package/skills/codex/examples/food-ordering/README.md +1 -0
- package/skills/codex/examples/food-ordering/package.json +1 -1
- package/skills/codex/examples/food-ordering/src/helpers.ts +1 -0
- package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +31 -27
- package/skills/codex/examples/food-ordering/src/views/ordering-result.ts +84 -0
- package/skills/codex/examples/food-ordering/test/ordering-flow.test.ts +187 -0
- package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
- package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
- package/skills/codex/references/embedded-assistant.md +10 -5
- package/skills/codex/references/widgets-and-apps.md +30 -10
- package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
|
@@ -26,13 +26,15 @@ Treat 1 MiB of uncompressed UTF-8 HTML as the recommended initial React-widget b
|
|
|
26
26
|
|
|
27
27
|
Every production widget handles loading, empty, partial, stale, error, retry, and success states. Prefill fields from known tool results and choose safe, reversible defaults; preserve the user’s work across rerenders, and never preselect a consequential action. Use public React primitives and branding tokens. Never author against `ns-*`, `nsr-*`, or example-local `--nw-*` classes/tokens; those are implementation details, not alternate design systems.
|
|
28
28
|
|
|
29
|
+
Keep the complete `useToolInfo()` result. An empty result envelope is pending, `isError` is an explicit tool error, and any non-pending success must validate every required field and identifier before it becomes UI data. Render a malformed-result error when validation fails, and render dependent actions only after validation succeeds. Safe local input defaults may follow valid hydration; they must never fabricate a loaded business record.
|
|
30
|
+
|
|
29
31
|
## React hook surface
|
|
30
32
|
|
|
31
33
|
Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
|
|
32
34
|
|
|
33
35
|
| Hook | Use for |
|
|
34
36
|
| :-- | :-- |
|
|
35
|
-
| `useToolInfo` | Read the invoking tool result
|
|
37
|
+
| `useToolInfo` | Read the complete invoking tool result: treat `{}` as pending, handle `isError`, validate every required `structuredContent` field and identifier, reject malformed success data, and render dependent actions only after validation succeeds. |
|
|
36
38
|
| `useCallTool` | Call a tool from the widget — returns `{ status, callTool, callToolAsync, data, structuredContent, error, reset }`; target a model-visible tool or a hidden `tool` helper. |
|
|
37
39
|
| `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
|
|
38
40
|
| `useLayout` | Read host layout: `{ theme, displayMode, locale?, host?, supports? }` (`displayMode` is `"inline"`/`"pip"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
|
|
@@ -65,14 +67,27 @@ const { useToolInfo, useCallTool, useViewState, useLayout, useOpenExternal, useS
|
|
|
65
67
|
generateHelpers<ServerDefinition>();
|
|
66
68
|
|
|
67
69
|
type OrderResult = {
|
|
68
|
-
readonly customer
|
|
69
|
-
readonly item
|
|
70
|
-
readonly total
|
|
71
|
-
readonly checkoutUrl
|
|
70
|
+
readonly customer: string;
|
|
71
|
+
readonly item: string;
|
|
72
|
+
readonly total: number;
|
|
73
|
+
readonly checkoutUrl: string;
|
|
72
74
|
};
|
|
73
75
|
|
|
76
|
+
function isOrderResult(value: unknown): value is OrderResult {
|
|
77
|
+
if (value === null || typeof value !== 'object') return false;
|
|
78
|
+
const result = value as Partial<OrderResult>;
|
|
79
|
+
return (
|
|
80
|
+
typeof result.customer === 'string' && result.customer.length > 0 &&
|
|
81
|
+
typeof result.item === 'string' && result.item.length > 0 &&
|
|
82
|
+
typeof result.total === 'number' && Number.isFinite(result.total) &&
|
|
83
|
+
typeof result.checkoutUrl === 'string' && result.checkoutUrl.startsWith('https://')
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
export default function OrderStatus() {
|
|
75
|
-
const
|
|
88
|
+
const toolInfo = useToolInfo('show_order');
|
|
89
|
+
const isPending = Object.keys(toolInfo).length === 0;
|
|
90
|
+
const shown = isOrderResult(toolInfo.structuredContent) ? toolInfo.structuredContent : undefined;
|
|
76
91
|
const placeOrder = useCallTool('place_order'); // calls the widget-only helper tool
|
|
77
92
|
const { supports } = useLayout();
|
|
78
93
|
const openExternal = useOpenExternal();
|
|
@@ -85,15 +100,16 @@ export default function OrderStatus() {
|
|
|
85
100
|
const checkoutUrl = shown?.checkoutUrl ?? '';
|
|
86
101
|
|
|
87
102
|
useEffect(() => {
|
|
88
|
-
if (!supports?.modelContext) return;
|
|
103
|
+
if (!shown || !supports?.modelContext) return;
|
|
89
104
|
void updateModelContext({
|
|
90
105
|
content: [{ type: 'text', text: `Pickup order: ${item}, total ${total}` }],
|
|
91
106
|
structuredContent: { widget: { name: 'pickup-order', lifecycle: 'active' }, order: { item, total } },
|
|
92
107
|
});
|
|
93
|
-
}, [item, supports?.modelContext, total, updateModelContext]);
|
|
108
|
+
}, [item, shown, supports?.modelContext, total, updateModelContext]);
|
|
94
109
|
|
|
95
110
|
async function submitOrder() {
|
|
96
|
-
|
|
111
|
+
if (!shown) return;
|
|
112
|
+
await placeOrder.callTool({ customer: shown.customer, item });
|
|
97
113
|
if (supports?.modelContext) {
|
|
98
114
|
await publishLifecycle('submitted', { surface: 'order', item, total, status: 'submitted' });
|
|
99
115
|
}
|
|
@@ -102,9 +118,13 @@ export default function OrderStatus() {
|
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
if (isPending) return <Feedback status="loading">Loading the order…</Feedback>;
|
|
122
|
+
if (toolInfo.isError) return <Feedback status="error">Could not load the order.</Feedback>;
|
|
123
|
+
if (!shown) return <Feedback status="error">The order result was incomplete.</Feedback>;
|
|
124
|
+
|
|
105
125
|
return (
|
|
106
126
|
// data-llm is an inspection hint; useUpdateModelContext above is the model-visible channel.
|
|
107
|
-
<Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown
|
|
127
|
+
<Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown.customer}: ${item}, total ${total}`}>
|
|
108
128
|
<Flow variant="stack">
|
|
109
129
|
<AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
|
|
110
130
|
<Region title="Order" description="Choose one item for pickup.">
|
|
@@ -3,7 +3,7 @@ name: reporting-noodle-feedback
|
|
|
3
3
|
description: "Use when a Noodle Seed bug, misleading instruction, missing capability, or concrete product improvement should be proposed to the user."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:0f404109f4845683 -->
|
|
7
7
|
|
|
8
8
|
# reporting-noodle-feedback
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: verifying-mcp-delivery
|
|
|
3
3
|
description: "Use when proving a Noodle Seed MCP project works at a named compile, local, connector, App, host, deployment, or production evidence level."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:6ef6ef551e26b78e -->
|
|
7
7
|
|
|
8
8
|
# verifying-mcp-delivery
|
|
9
9
|
|
package/skills/codex/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: noodle-seed
|
|
|
3
3
|
description: "Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:cd6ca0d915e6acb9 -->
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: authoring-mcp-servers
|
|
|
3
3
|
description: "Use when creating or extending a headless Noodle Seed MCP server, tool, resource, prompt, or typed model-facing capability."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:0b2fd8c7e43fc69f -->
|
|
7
7
|
|
|
8
8
|
# authoring-mcp-servers
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: building-mcp-apps
|
|
|
3
3
|
description: "Use when a Noodle Seed MCP App, widget, interactive card, visual interaction, or host-visible UI is the primary requested outcome."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:f7fa54992c8d7692 -->
|
|
7
7
|
|
|
8
8
|
# building-mcp-apps
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: connecting-apis-to-mcp
|
|
|
3
3
|
description: "Use when credentials, an API URL, an OpenAPI document, or an observed response must become real Noodle Seed MCP behavior."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:1e86b8704f407bd3 -->
|
|
7
7
|
|
|
8
8
|
# connecting-apis-to-mcp
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: debugging-mcp-delivery
|
|
|
3
3
|
description: "Use when an existing Noodle Seed MCP project has a concrete validation, runtime, connector, App, host, deployment, or production failure."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:aa715bae12041d7c -->
|
|
7
7
|
|
|
8
8
|
# debugging-mcp-delivery
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: deploying-mcp-services
|
|
|
3
3
|
description: "Use when the user explicitly requests a Noodle Seed hosted link, configuration write, deployment, access change, rollback, or connection write."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:93e735b7ffb45df1 -->
|
|
7
7
|
|
|
8
8
|
# deploying-mcp-services
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: designing-mcp-products
|
|
|
3
3
|
description: "Use when a Noodle Seed MCP product idea needs conversational fit, user benefit, scope, interaction, or evidence design before implementation."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:76cce86729cffbee -->
|
|
7
7
|
|
|
8
8
|
# designing-mcp-products
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: embedding-mcp-assistants
|
|
|
3
3
|
description: "Use when embedding a Noodle assistant into an existing SaaS or web application with browser, identity, session, and credential boundaries."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:5d8f40f904d6ab4b -->
|
|
7
7
|
|
|
8
8
|
# embedding-mcp-assistants
|
|
9
9
|
|
|
@@ -205,6 +205,7 @@ and returns the AI SDK transcript plus the canonical client commands:
|
|
|
205
205
|
'use client';
|
|
206
206
|
|
|
207
207
|
import { useState } from 'react';
|
|
208
|
+
import { NoodleAppView } from '@noodleseed/assistant/react';
|
|
208
209
|
import { useNoodleAssistant } from '@noodleseed/assistant/react/client';
|
|
209
210
|
|
|
210
211
|
export function CustomerAssistant({ principalKey }: { principalKey: string }) {
|
|
@@ -274,9 +275,11 @@ export function CustomerAssistant({ principalKey }: { principalKey: string }) {
|
|
|
274
275
|
}
|
|
275
276
|
if (part.type === 'data-view') {
|
|
276
277
|
return (
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
<NoodleAppView
|
|
279
|
+
key={`${part.data.id}:${part.data.resourceUri}`}
|
|
280
|
+
client={client}
|
|
281
|
+
view={part.data}
|
|
282
|
+
/>
|
|
280
283
|
);
|
|
281
284
|
}
|
|
282
285
|
return <p key={index}>Unsupported assistant content.</p>;
|
|
@@ -315,8 +318,12 @@ export function CustomerAssistant({ principalKey }: { principalKey: string }) {
|
|
|
315
318
|
then aborts and clears the prior session and transcript. The sample fails closed on input requests until its
|
|
316
319
|
fallback is replaced with a form generated from `requestedSchema`. A production renderer must show the
|
|
317
320
|
complete confirmation review and both decisions. For `data-view`, map `resourceUri` or `tool` and the
|
|
318
|
-
bounded/redacted result to a component already trusted by this application
|
|
319
|
-
|
|
321
|
+
bounded/redacted result to a component already trusted by this application only when intentionally replacing
|
|
322
|
+
the linked App with a native UI. Otherwise use `NoodleAppView`; JSON result data is not the App UI. Its
|
|
323
|
+
semantic lifecycle identity is the client plus `view.id` plus `view.resourceUri`, so parent payload/callback
|
|
324
|
+
rerenders keep the iframe and only a different view or unmount tears down the bridge.
|
|
325
|
+
Never inject `part.data.html`, assign it to `srcdoc`, fetch a `ui://` URI, or reproduce the bridge directly. Pages with a
|
|
326
|
+
Content-Security-Policy must include the Noodle service origin in both `connect-src` and `frame-src`.
|
|
320
327
|
|
|
321
328
|
Outside React, subscribe to the DOM-free client directly. It exposes the same conversation as headless AI
|
|
322
329
|
SDK `UIMessage` state, including typed confirmation, input, tool-result, and linked-view parts:
|
|
@@ -24,6 +24,7 @@ private customer data.
|
|
|
24
24
|
| Model-visible widget state | `useUpdateModelContext` publishes one cohesive replacement snapshot when supported; `useWidgetLifecycle` auto-publishes mounted/cancelled/dismissed and reports author-owned submitted milestones for future context (not host-presentation proof), while the user-triggered submit pairs `useSendFollowUpMessage` for an immediate reply |
|
|
25
25
|
| Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
|
|
26
26
|
| Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
|
|
27
|
+
| Fail-closed hydration | The React view treats only the unhydrated, pre-result `{}` envelope as pending; a hydrated empty success remains distinct. It surfaces `isError`, validates required records and identifiers, and withholds ordering actions from malformed results |
|
|
27
28
|
|
|
28
29
|
The example is intentionally richer than the generated starter, but each inline view still follows the
|
|
29
30
|
same default: one immediate purpose, one primary action, at most one subordinate action, and progressive
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createViewStore,
|
|
8
8
|
DataCard,
|
|
9
9
|
DataList,
|
|
10
|
+
Feedback,
|
|
10
11
|
Field,
|
|
11
12
|
HandoffButton,
|
|
12
13
|
QuantityStepper,
|
|
@@ -25,30 +26,11 @@ import {
|
|
|
25
26
|
View,
|
|
26
27
|
ViewStack,
|
|
27
28
|
} from '../helpers.js';
|
|
29
|
+
import { isOrderingEntryResult, type MenuItem, type Store } from './ordering-result.js';
|
|
28
30
|
import './widget-style.css';
|
|
29
31
|
|
|
30
32
|
type ViewName = 'stores' | 'menu' | 'item' | 'cart' | 'review' | 'handoff';
|
|
31
33
|
|
|
32
|
-
type Store = {
|
|
33
|
-
readonly id: string;
|
|
34
|
-
readonly name: string;
|
|
35
|
-
readonly cuisine: string;
|
|
36
|
-
readonly address: string;
|
|
37
|
-
readonly open: boolean;
|
|
38
|
-
readonly etaMinutes: number;
|
|
39
|
-
readonly rating: number;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type MenuItem = {
|
|
43
|
-
readonly id: string;
|
|
44
|
-
readonly storeId: string;
|
|
45
|
-
readonly category: string;
|
|
46
|
-
readonly name: string;
|
|
47
|
-
readonly price: number;
|
|
48
|
-
readonly description: string;
|
|
49
|
-
readonly modifiers: readonly string[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
34
|
type CartLine = {
|
|
53
35
|
readonly itemId: string;
|
|
54
36
|
readonly quantity: number;
|
|
@@ -93,12 +75,11 @@ function modifierLabel(value: string): string {
|
|
|
93
75
|
|
|
94
76
|
export default function OrderingFlow() {
|
|
95
77
|
const { displayMode, supports, theme } = useLayout();
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}>(useToolInfo('open_ordering'));
|
|
78
|
+
const toolInfo = useToolInfo('open_ordering');
|
|
79
|
+
const isPending = Object.keys(toolInfo).length === 0;
|
|
80
|
+
const entry = isOrderingEntryResult(toolInfo.structuredContent)
|
|
81
|
+
? toolInfo.structuredContent
|
|
82
|
+
: undefined;
|
|
102
83
|
const flow = useAppFlow<ViewName>({
|
|
103
84
|
key: 'ordering_flow',
|
|
104
85
|
initialView: 'stores',
|
|
@@ -178,7 +159,7 @@ export default function OrderingFlow() {
|
|
|
178
159
|
const canUpdateModelContext = supports?.modelContext === true;
|
|
179
160
|
|
|
180
161
|
useEffect(() => {
|
|
181
|
-
if (!canUpdateModelContext) return;
|
|
162
|
+
if (!entry || !canUpdateModelContext) return;
|
|
182
163
|
// Each call replaces model context, so publish one cohesive snapshot of current surface state.
|
|
183
164
|
void updateModelContext({
|
|
184
165
|
content: [{ type: 'text', text: `Food ordering: ${llmSummary}; ${lifecycle}.` }],
|
|
@@ -199,6 +180,7 @@ export default function OrderingFlow() {
|
|
|
199
180
|
cart.lines.length,
|
|
200
181
|
cart.subtotal,
|
|
201
182
|
customer,
|
|
183
|
+
entry,
|
|
202
184
|
lifecycle,
|
|
203
185
|
llmSummary,
|
|
204
186
|
selectedItemId,
|
|
@@ -304,6 +286,28 @@ export default function OrderingFlow() {
|
|
|
304
286
|
}
|
|
305
287
|
}
|
|
306
288
|
|
|
289
|
+
if (isPending) {
|
|
290
|
+
return (
|
|
291
|
+
<Feedback status="loading" title="Loading">
|
|
292
|
+
Loading food ordering…
|
|
293
|
+
</Feedback>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (toolInfo.isError) {
|
|
297
|
+
return (
|
|
298
|
+
<Feedback status="error" title="Ordering unavailable">
|
|
299
|
+
Could not load food ordering.
|
|
300
|
+
</Feedback>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (!entry) {
|
|
304
|
+
return (
|
|
305
|
+
<Feedback status="error" title="Invalid ordering result">
|
|
306
|
+
Food ordering result was incomplete.
|
|
307
|
+
</Feedback>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
307
311
|
return (
|
|
308
312
|
<AppShell
|
|
309
313
|
className={`nw-shell${theme === 'dark' ? ' dark' : ''}`}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type Store = {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly cuisine: string;
|
|
5
|
+
readonly address: string;
|
|
6
|
+
readonly open: boolean;
|
|
7
|
+
readonly etaMinutes: number;
|
|
8
|
+
readonly rating: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type MenuItem = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly storeId: string;
|
|
14
|
+
readonly category: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly price: number;
|
|
17
|
+
readonly description: string;
|
|
18
|
+
readonly modifiers: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type OrderingEntryResult = {
|
|
22
|
+
readonly customer: string;
|
|
23
|
+
readonly stores: readonly Store[];
|
|
24
|
+
readonly featuredItems: readonly MenuItem[];
|
|
25
|
+
readonly status: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function nonEmptyString(value: unknown): value is string {
|
|
29
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isStore(value: unknown): value is Store {
|
|
33
|
+
if (value === null || typeof value !== 'object') return false;
|
|
34
|
+
const store = value as Partial<Store>;
|
|
35
|
+
return (
|
|
36
|
+
nonEmptyString(store.id) &&
|
|
37
|
+
nonEmptyString(store.name) &&
|
|
38
|
+
nonEmptyString(store.cuisine) &&
|
|
39
|
+
nonEmptyString(store.address) &&
|
|
40
|
+
typeof store.open === 'boolean' &&
|
|
41
|
+
typeof store.etaMinutes === 'number' &&
|
|
42
|
+
Number.isFinite(store.etaMinutes) &&
|
|
43
|
+
typeof store.rating === 'number' &&
|
|
44
|
+
Number.isFinite(store.rating)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isMenuItem(value: unknown): value is MenuItem {
|
|
49
|
+
if (value === null || typeof value !== 'object') return false;
|
|
50
|
+
const item = value as Partial<MenuItem>;
|
|
51
|
+
return (
|
|
52
|
+
nonEmptyString(item.id) &&
|
|
53
|
+
nonEmptyString(item.storeId) &&
|
|
54
|
+
nonEmptyString(item.category) &&
|
|
55
|
+
nonEmptyString(item.name) &&
|
|
56
|
+
typeof item.price === 'number' &&
|
|
57
|
+
Number.isFinite(item.price) &&
|
|
58
|
+
nonEmptyString(item.description) &&
|
|
59
|
+
Array.isArray(item.modifiers) &&
|
|
60
|
+
item.modifiers.every(nonEmptyString)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isOrderingEntryResult(value: unknown): value is OrderingEntryResult {
|
|
65
|
+
if (value === null || typeof value !== 'object') return false;
|
|
66
|
+
const entry = value as Partial<OrderingEntryResult>;
|
|
67
|
+
if (
|
|
68
|
+
!nonEmptyString(entry.customer) ||
|
|
69
|
+
!nonEmptyString(entry.status) ||
|
|
70
|
+
!Array.isArray(entry.stores) ||
|
|
71
|
+
!entry.stores.every(isStore) ||
|
|
72
|
+
!Array.isArray(entry.featuredItems) ||
|
|
73
|
+
!entry.featuredItems.every(isMenuItem)
|
|
74
|
+
) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const storeIds = new Set(entry.stores.map((store) => store.id));
|
|
78
|
+
const itemIds = new Set(entry.featuredItems.map((item) => item.id));
|
|
79
|
+
return (
|
|
80
|
+
storeIds.size === entry.stores.length &&
|
|
81
|
+
itemIds.size === entry.featuredItems.length &&
|
|
82
|
+
entry.featuredItems.every((item) => storeIds.has(item.storeId))
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
/// <reference lib="dom" />
|
|
3
|
+
import { act, createElement as h } from 'react';
|
|
4
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import OrderingFlow from '../src/views/ordering-flow.js';
|
|
7
|
+
|
|
8
|
+
vi.mock('../src/helpers.js', async () => {
|
|
9
|
+
const { createElement } = await import('react');
|
|
10
|
+
const container = ({ children }: { readonly children?: unknown }) =>
|
|
11
|
+
createElement('div', null, children);
|
|
12
|
+
const button = ({ children }: { readonly children?: unknown }) =>
|
|
13
|
+
createElement('button', { type: 'button' }, children);
|
|
14
|
+
return {
|
|
15
|
+
ActionBar: container,
|
|
16
|
+
AppShell: ({
|
|
17
|
+
children,
|
|
18
|
+
footer,
|
|
19
|
+
subtitle,
|
|
20
|
+
title,
|
|
21
|
+
}: {
|
|
22
|
+
readonly children?: unknown;
|
|
23
|
+
readonly footer?: unknown;
|
|
24
|
+
readonly subtitle?: string;
|
|
25
|
+
readonly title?: string;
|
|
26
|
+
}) => createElement('main', null, title, subtitle, children, footer),
|
|
27
|
+
AsyncBoundary: container,
|
|
28
|
+
ChoiceGroup: container,
|
|
29
|
+
createViewStore:
|
|
30
|
+
<T>(_key: string, initial: T) =>
|
|
31
|
+
(selector?: (state: T) => unknown) => ({
|
|
32
|
+
state: initial,
|
|
33
|
+
selected: selector?.(initial),
|
|
34
|
+
setState: () => undefined,
|
|
35
|
+
}),
|
|
36
|
+
DataCard: button,
|
|
37
|
+
DataList: container,
|
|
38
|
+
Feedback: ({ children, status }: { readonly children?: unknown; readonly status?: string }) =>
|
|
39
|
+
createElement('div', { 'data-status': status }, children),
|
|
40
|
+
Field: container,
|
|
41
|
+
HandoffButton: button,
|
|
42
|
+
QuantityStepper: container,
|
|
43
|
+
ShellNav: container,
|
|
44
|
+
StatusBadge: container,
|
|
45
|
+
SubmitButton: button,
|
|
46
|
+
useAppFlow: () => ({
|
|
47
|
+
activeView: 'stores',
|
|
48
|
+
navigate: () => undefined,
|
|
49
|
+
back: () => undefined,
|
|
50
|
+
canBack: false,
|
|
51
|
+
params: {},
|
|
52
|
+
}),
|
|
53
|
+
useCallTool: () => ({
|
|
54
|
+
status: 'idle',
|
|
55
|
+
isIdle: true,
|
|
56
|
+
isPending: false,
|
|
57
|
+
isSuccess: false,
|
|
58
|
+
isError: false,
|
|
59
|
+
callTool: () => Promise.resolve({ structuredContent: {} }),
|
|
60
|
+
callToolAsync: () => Promise.resolve({ structuredContent: {} }),
|
|
61
|
+
reset: () => undefined,
|
|
62
|
+
}),
|
|
63
|
+
useHandoff: () => ({ status: 'idle', open: () => Promise.resolve() }),
|
|
64
|
+
useLayout: () => ({
|
|
65
|
+
theme: 'light',
|
|
66
|
+
displayMode: 'inline',
|
|
67
|
+
supports: { modelContext: false, followUpMessage: false },
|
|
68
|
+
}),
|
|
69
|
+
useSendFollowUpMessage: () => () => Promise.resolve(),
|
|
70
|
+
useToolInfo: () => toolResult,
|
|
71
|
+
useUpdateModelContext: () => () => Promise.resolve(),
|
|
72
|
+
useViewState: <T>(_key: string, initial: T) => [initial, () => undefined] as const,
|
|
73
|
+
useWidgetLifecycle: () => () => Promise.resolve(),
|
|
74
|
+
View: container,
|
|
75
|
+
ViewStack: container,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
type ToolResult = {
|
|
80
|
+
readonly content?: unknown;
|
|
81
|
+
readonly structuredContent?: unknown;
|
|
82
|
+
readonly _meta?: unknown;
|
|
83
|
+
readonly isError?: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let toolResult: ToolResult;
|
|
87
|
+
let root: Root | undefined;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
91
|
+
toolResult = {};
|
|
92
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
93
|
+
(globalThis as { __noodleReactVersion?: number }).__noodleReactVersion = 0;
|
|
94
|
+
(globalThis as { __noodleReactBridge?: unknown }).__noodleReactBridge = {
|
|
95
|
+
getToolResult: () => toolResult,
|
|
96
|
+
getViewState: () => ({}),
|
|
97
|
+
setWidgetState: () => undefined,
|
|
98
|
+
getLayout: () => ({
|
|
99
|
+
theme: 'light',
|
|
100
|
+
displayMode: 'inline',
|
|
101
|
+
supports: { modelContext: false, followUpMessage: false },
|
|
102
|
+
}),
|
|
103
|
+
callServerTool: () => Promise.resolve({ structuredContent: {} }),
|
|
104
|
+
openExternal: () => Promise.resolve(),
|
|
105
|
+
sendFollowUpMessage: () => Promise.resolve(),
|
|
106
|
+
updateModelContext: () => Promise.resolve(),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
if (root) act(() => root?.unmount());
|
|
112
|
+
root = undefined;
|
|
113
|
+
delete (globalThis as { __noodleReactBridge?: unknown }).__noodleReactBridge;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function render(result: ToolResult): string {
|
|
117
|
+
toolResult = result;
|
|
118
|
+
root = createRoot(document.querySelector('#root') as HTMLElement);
|
|
119
|
+
act(() => root?.render(h(OrderingFlow)));
|
|
120
|
+
return document.body.textContent ?? '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe('food-ordering invoking result states', () => {
|
|
124
|
+
it('renders pending without identifier-dependent actions before hydration', () => {
|
|
125
|
+
const text = render({});
|
|
126
|
+
expect(text).toContain('Loading food ordering');
|
|
127
|
+
expect(text).not.toContain('Search stores');
|
|
128
|
+
expect(document.querySelector('button')).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders an explicit tool error without ordering actions', () => {
|
|
132
|
+
const text = render({
|
|
133
|
+
content: [{ type: 'text', text: 'Ordering is unavailable' }],
|
|
134
|
+
isError: true,
|
|
135
|
+
});
|
|
136
|
+
expect(text).toContain('Could not load food ordering');
|
|
137
|
+
expect(text).not.toContain('Search stores');
|
|
138
|
+
expect(document.querySelector('button')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects malformed success data before rendering identifier-dependent actions', () => {
|
|
142
|
+
const text = render({
|
|
143
|
+
structuredContent: {
|
|
144
|
+
status: 'Ready',
|
|
145
|
+
customer: 'Asha',
|
|
146
|
+
stores: [{ name: 'Missing identifier' }],
|
|
147
|
+
featuredItems: [],
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
expect(text).toContain('Food ordering result was incomplete');
|
|
151
|
+
expect(text).not.toContain('Search stores');
|
|
152
|
+
expect(document.querySelector('button')).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('preserves the ordering flow after valid hydration', () => {
|
|
156
|
+
const text = render({
|
|
157
|
+
structuredContent: {
|
|
158
|
+
status: 'Ready to build a food order.',
|
|
159
|
+
customer: 'Asha',
|
|
160
|
+
stores: [
|
|
161
|
+
{
|
|
162
|
+
id: 'harbor-noodles',
|
|
163
|
+
name: 'Harbor Noodles',
|
|
164
|
+
cuisine: 'Noodles',
|
|
165
|
+
address: '18 Pier Lane',
|
|
166
|
+
open: true,
|
|
167
|
+
etaMinutes: 24,
|
|
168
|
+
rating: 4.8,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
featuredItems: [
|
|
172
|
+
{
|
|
173
|
+
id: 'spicy_miso',
|
|
174
|
+
storeId: 'harbor-noodles',
|
|
175
|
+
category: 'Bowls',
|
|
176
|
+
name: 'Spicy Miso Bowl',
|
|
177
|
+
price: 16,
|
|
178
|
+
description: 'Miso broth and noodles.',
|
|
179
|
+
modifiers: ['extra_noodles'],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
expect(text).toContain('Search stores');
|
|
185
|
+
expect(text).toContain('Harbor Noodles');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -3,7 +3,7 @@ name: executing-noodle-plans
|
|
|
3
3
|
description: "Use when the user asks to execute an approved, decision-complete implementation plan for a Noodle Seed project task by task with test-first changes, review, recovery, and final verification."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:6a9f132ddb79352e -->
|
|
7
7
|
|
|
8
8
|
# Execute a Noodle Seed implementation plan
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: publishing-mcp-integrations
|
|
|
3
3
|
description: "Use when preparing, reviewing, or submitting a Noodle Seed MCP integration to a host or app directory."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.45.0 hash:efffbf82007f935d -->
|
|
7
7
|
|
|
8
8
|
# publishing-mcp-integrations
|
|
9
9
|
|