@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.
Files changed (44) hide show
  1. package/manifest.json +279 -247
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/authoring-mcp-servers/SKILL.md +1 -1
  5. package/skills/claude-code/building-mcp-apps/SKILL.md +1 -1
  6. package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
  7. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
  8. package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
  9. package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
  10. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
  11. package/skills/claude-code/examples/customer-auth/README.md +12 -5
  12. package/skills/claude-code/examples/food-ordering/README.md +1 -0
  13. package/skills/claude-code/examples/food-ordering/package.json +1 -1
  14. package/skills/claude-code/examples/food-ordering/src/helpers.ts +1 -0
  15. package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +31 -27
  16. package/skills/claude-code/examples/food-ordering/src/views/ordering-result.ts +84 -0
  17. package/skills/claude-code/examples/food-ordering/test/ordering-flow.test.ts +187 -0
  18. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  19. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
  20. package/skills/claude-code/references/embedded-assistant.md +10 -5
  21. package/skills/claude-code/references/widgets-and-apps.md +30 -10
  22. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
  23. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
  24. package/skills/codex/SKILL.md +1 -1
  25. package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
  26. package/skills/codex/building-mcp-apps/SKILL.md +1 -1
  27. package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
  28. package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
  29. package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
  30. package/skills/codex/designing-mcp-products/SKILL.md +1 -1
  31. package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
  32. package/skills/codex/examples/customer-auth/README.md +12 -5
  33. package/skills/codex/examples/food-ordering/README.md +1 -0
  34. package/skills/codex/examples/food-ordering/package.json +1 -1
  35. package/skills/codex/examples/food-ordering/src/helpers.ts +1 -0
  36. package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +31 -27
  37. package/skills/codex/examples/food-ordering/src/views/ordering-result.ts +84 -0
  38. package/skills/codex/examples/food-ordering/test/ordering-flow.test.ts +187 -0
  39. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  40. package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
  41. package/skills/codex/references/embedded-assistant.md +10 -5
  42. package/skills/codex/references/widgets-and-apps.md +30 -10
  43. package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
  44. package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/agent-kit",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "private": false,
5
5
  "description": "Self-checking, self-updating agent skills for the Noodle Seed CLI. Authored in this repo by @noodle-borg/agent-kit; this is the published, independently-versioned canonical skills artifact the CLI fetches and verifies.",
6
6
  "license": "Apache-2.0",
@@ -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.44.0 hash:cd6ca0d915e6acb9 -->
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.44.0 hash:0b2fd8c7e43fc69f -->
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.44.0 hash:f7fa54992c8d7692 -->
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.44.0 hash:1e86b8704f407bd3 -->
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.44.0 hash:aa715bae12041d7c -->
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.44.0 hash:93e735b7ffb45df1 -->
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.44.0 hash:76cce86729cffbee -->
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.44.0 hash:5d8f40f904d6ab4b -->
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
- <p key={part.data.id}>
278
- Trusted app view available: {part.data.title ?? part.data.resourceUri}
279
- </p>
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. Never inject `part.data.html`,
319
- assign it to `srcdoc`, or fetch a `ui://` URI.
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
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "vitest run test/server.test.ts",
7
+ "test": "vitest run test",
8
8
  "validate": "noodle validate",
9
9
  "dev": "noodle dev",
10
10
  "deploy": "noodle deploy"
@@ -8,6 +8,7 @@ export {
8
8
  createViewStore,
9
9
  DataCard,
10
10
  DataList,
11
+ Feedback,
11
12
  Field,
12
13
  HandoffButton,
13
14
  QuantityStepper,
@@ -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 entry = structured<{
97
- readonly customer?: string;
98
- readonly stores?: readonly Store[];
99
- readonly featuredItems?: readonly MenuItem[];
100
- readonly status?: string;
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.44.0 hash:6a9f132ddb79352e -->
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.44.0 hash:efffbf82007f935d -->
6
+ <!-- noodle-skill version:0.45.0 hash:efffbf82007f935d -->
7
7
 
8
8
  # publishing-mcp-integrations
9
9
 
@@ -254,6 +254,7 @@ For a customer-owned React renderer, use the renderer-free hook. It owns client
254
254
  "use client";
255
255
 
256
256
  import { useState } from "react";
257
+ import { NoodleAppView } from "@noodleseed/assistant/react";
257
258
  import { useNoodleAssistant } from "@noodleseed/assistant/react/client";
258
259
 
259
260
  export function CustomAssistant({ principalKey }: { principalKey: string }) {
@@ -327,9 +328,11 @@ export function CustomAssistant({ principalKey }: { principalKey: string }) {
327
328
  }
328
329
  if (part.type === "data-view") {
329
330
  return (
330
- <p key={part.data.id}>
331
- Trusted app view available: {part.data.title ?? part.data.resourceUri}
332
- </p>
331
+ <NoodleAppView
332
+ key={`${part.data.id}:${part.data.resourceUri}`}
333
+ client={client}
334
+ view={part.data}
335
+ />
333
336
  );
334
337
  }
335
338
  return <p key={index}>Unsupported assistant content.</p>;
@@ -364,7 +367,9 @@ export function CustomAssistant({ principalKey }: { principalKey: string }) {
364
367
 
365
368
  `principalKey` is a browser-local identity for the authenticated user/tenant and is never sent to Noodle. Change it whenever that principal changes; the hook then aborts and clears the previous session and transcript. The hook does not register `<noodle-assistant>` or render Noodle markup.
366
369
 
367
- The sample fails closed on input requests until you replace that branch with a form generated from `requestedSchema`. A custom renderer must show the complete confirmation review and both decisions, handle every part it supports, and surface an explicit unsupported state for the rest. For `data-view`, map `resourceUri` or `tool` plus the bounded/redacted `result` to a component already trusted by the application. Never inject `part.data.html`, assign it to `srcdoc`, or fetch a `ui://` URI; the managed element alone supplies Noodle’s sandbox host. Do not wrap this client in another chat transport or invent user messages for interaction continuations.
370
+ The sample fails closed on input requests until you replace that branch with a form generated from `requestedSchema`. A custom renderer must show the complete confirmation review and both decisions, handle every part it supports, and surface an explicit unsupported state for the rest. For `data-view`, use `NoodleAppView` to render the linked App or deliberately map `resourceUri`/tool plus the bounded redacted `result` to an application-trusted native component. JSON result data is not the linked App UI. Never inject `part.data.html`, assign it to `srcdoc`, fetch a `ui://` URI, or reproduce the bridge with a direct Ext Apps dependency. Do not wrap this client in another chat transport or invent user messages for interaction continuations.
371
+
372
+ `NoodleAppView` owns one bridge for the semantic view identity: client + `view.id` + `view.resourceUri`. It retains the iframe across fresh payload/callback objects from parent rerenders, reads current payloads through refs, and sends standard App teardown only when that semantic identity changes or the component unmounts. Do not key an ancestor by a view object or callback. If the embedding page sets Content-Security-Policy, include the Noodle service origin in both `connect-src` and `frame-src`.
368
373
 
369
374
  Outside React, use the same DOM-free client directly. It keeps the session token in memory, exposes a React-free `UIMessage` transcript with typed parts, and never registers a custom element:
370
375
 
@@ -416,7 +421,7 @@ if (pending) {
416
421
 
417
422
  `subscribeChat` immediately emits a detached `{ messages, status, error? }` snapshot and then emits as `UIMessage.parts` change. Text uses `text`; Noodle confirmations, input requests, tool results, and linked views use `data-confirmation`, `data-input-request`, `data-tool-result`, and `data-view`. Interaction data moves through pending/submitting/accepted/declined/cancelled. Use raw `subscribe(...)` only for transport/session lifecycle events that are not transcript content.
418
423
 
419
- `data-view` means a completed tool has a linked MCP App view. It carries the call/interaction id, tool, `ui://` identity, optional title, bounded/redacted public result, and—on current services—the self-contained bridged document. The standard element is an MCP Apps host and mounts that document behind a double iframe; a customer renderer ignores it and maps the identity/result to an application-trusted component. The standard element supports lifecycle, app tool/resource calls, ui/message, ui/update-model-context, links, resize, and inline/fullscreen; sampling, tasks, downloads, and remote DOM are not advertised. It also dispatches `assistant-view-available` for a customer-owned renderer.
424
+ `data-view` means a completed tool has a linked MCP App view. In a customer-owned React renderer, pass that typed part and the existing client to `NoodleAppView`; it retains one bridge for client + `view.id` + `view.resourceUri` and requests standard App teardown on semantic replacement or unmount. Deliberately map the bounded result to an application-trusted native component only when replacing the linked App UI.
420
425
 
421
426
  `clientContext` and typed `pageContext` are recomputed for each turn. `updateContext(...)` remains the legacy session-exchange context; `updatePageContext(...)` replaces the fresh per-turn application hint. `updateModelContext({ content, structuredContent })` publishes one cohesive renderer snapshot for later message turns without starting a turn; every call replaces the prior snapshot rather than merging fields. These are untrusted data, not conversation history or authorization input, and the boundaries reject credential-shaped or unbounded updates. A message may re-exchange once after a pre-execution `401`; the client never auto-retries interaction decisions. `tool_proposed.arguments` is a complete schema-aware review projection and, for connector-backed tools, names the sole exact connector version/operation/resolved arguments. Sensitive/write-only fields are redacted; truncating or omitting any non-sensitive action field fails closed. Accept is bound to the server-held action and claims at most one execution attempt—clients cannot replace it. Normal terminal outcomes scrub private arguments and continuations immediately; only an accepted action still executing retains them for the one-hour unknown-outcome recovery window, after which it records `interaction_outcome_unknown` and scrubs. Without downstream idempotency this is not an exactly-once business-effect guarantee. To reconcile a lost response, explicitly repeat the same id and decision: the service returns its durable stored outcome without re-execution.
422
427