@noodleseed/agent-kit 0.21.1 → 0.23.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 +83 -33
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/claude-code/examples/acme-bistro/src/server.ts +1 -1
- package/skills/claude-code/examples/acme-discovery/src/server.ts +1 -1
- package/skills/claude-code/examples/acme-tasks/src/server.ts +4 -2
- package/skills/claude-code/examples/acme-tasks/test/server.test.ts +9 -0
- package/skills/claude-code/examples/customer-auth/README.md +237 -0
- package/skills/claude-code/examples/customer-auth/noodle.json +4 -0
- package/skills/claude-code/examples/customer-auth/package.json +16 -0
- package/skills/claude-code/examples/customer-auth/src/server.ts +145 -0
- package/skills/claude-code/examples/customer-auth/test/server.test.ts +25 -0
- package/skills/claude-code/examples/food-ordering/README.md +7 -3
- package/skills/claude-code/examples/food-ordering/src/helpers.ts +2 -0
- package/skills/claude-code/examples/food-ordering/src/server.ts +47 -2
- package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +56 -3
- package/skills/claude-code/examples/food-ordering/test/server.test.ts +46 -0
- package/skills/claude-code/references/authoring-workflow.md +117 -2
- package/skills/claude-code/references/compile-errors.md +6 -1
- package/skills/claude-code/references/embedded-assistant.md +95 -10
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/claude-code/references/sdk-surface.md +2 -2
- package/skills/claude-code/references/widgets-and-apps.md +32 -7
- package/skills/codex/SKILL.md +1 -1
- package/skills/codex/examples/acme-bistro/src/server.ts +1 -1
- package/skills/codex/examples/acme-discovery/src/server.ts +1 -1
- package/skills/codex/examples/acme-tasks/src/server.ts +4 -2
- package/skills/codex/examples/acme-tasks/test/server.test.ts +9 -0
- package/skills/codex/examples/customer-auth/README.md +237 -0
- package/skills/codex/examples/customer-auth/noodle.json +4 -0
- package/skills/codex/examples/customer-auth/package.json +16 -0
- package/skills/codex/examples/customer-auth/src/server.ts +145 -0
- package/skills/codex/examples/customer-auth/test/server.test.ts +25 -0
- package/skills/codex/examples/food-ordering/README.md +7 -3
- package/skills/codex/examples/food-ordering/src/helpers.ts +2 -0
- package/skills/codex/examples/food-ordering/src/server.ts +47 -2
- package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +56 -3
- package/skills/codex/examples/food-ordering/test/server.test.ts +46 -0
- package/skills/codex/references/authoring-workflow.md +117 -2
- package/skills/codex/references/compile-errors.md +6 -1
- package/skills/codex/references/embedded-assistant.md +95 -10
- package/skills/codex/references/examples.md +1 -1
- package/skills/codex/references/sdk-surface.md +2 -2
- package/skills/codex/references/widgets-and-apps.md +32 -7
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
- Access modes and customer auth
|
|
9
9
|
- Create the backend client
|
|
10
10
|
- Integrate the customer backend
|
|
11
|
+
- Ground time and ambient facts
|
|
11
12
|
- Verified session context (identity and claims)
|
|
12
13
|
- The session response
|
|
13
|
-
-
|
|
14
|
+
- Choose a browser renderer
|
|
14
15
|
- Toolchain requirements
|
|
15
16
|
- Verify the boundary
|
|
16
17
|
- Troubleshooting: symptom to diagnosis
|
|
@@ -32,6 +33,7 @@ Use the same server tools in the embed; do not create a second tool set. Declare
|
|
|
32
33
|
|
|
33
34
|
```ts
|
|
34
35
|
branding: { name: "Acme", accent: "#3157D5" },
|
|
36
|
+
context: { defaults: { locale: "en-GB", timeZone: "Europe/London" } },
|
|
35
37
|
assistant: embeddedAssistant({
|
|
36
38
|
model: openAICompatible({
|
|
37
39
|
baseUrl: variable("ASSISTANT_MODEL_BASE_URL"),
|
|
@@ -105,6 +107,8 @@ export async function POST(request: Request) {
|
|
|
105
107
|
origin: process.env.PUBLIC_APP_ORIGIN!,
|
|
106
108
|
user: { id: user.id, email: user.email, roles: user.roles },
|
|
107
109
|
context,
|
|
110
|
+
// Saved, backend-verified user preferences outrank browser hints.
|
|
111
|
+
preferences: { locale: user.locale, timeZone: user.timeZone },
|
|
108
112
|
});
|
|
109
113
|
return Response.json(session);
|
|
110
114
|
}
|
|
@@ -114,6 +118,34 @@ Authenticate before exchange. Source `origin` from trusted server configuration
|
|
|
114
118
|
|
|
115
119
|
`serviceUrl` is the Noodle Seed control-plane base URL: the value `noodle assistant clients create` prints, also stored as `serviceUrl` in `deployment.json`. It is NOT the deployment MCP endpoint (`url`, which ends in `/v1/mcp` and rejects session exchange). Never probe or guess endpoints with real credentials.
|
|
116
120
|
|
|
121
|
+
## Ground time and ambient facts
|
|
122
|
+
|
|
123
|
+
Every assistant turn receives a server-authoritative instant and user-local date/time. Locale and IANA time zone resolve in this order: backend-verified `preferences` from session exchange, fresh per-turn browser `clientContext` hints, `server.context.defaults`, then platform defaults (`en-US`/`UTC`). Browser hints affect presentation and relative-date interpretation only; they are untrusted and never authorize a tool.
|
|
124
|
+
|
|
125
|
+
Use the server-level context declaration for application facts that every surface should share:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
context: {
|
|
129
|
+
defaults: { locale: 'en-GB', timeZone: 'Europe/London' },
|
|
130
|
+
ambient: {
|
|
131
|
+
output: z.object({ defaultTeamId: z.string(), holidays: z.array(z.string()) }),
|
|
132
|
+
fulfil: ({ user, context, connectors }) => {
|
|
133
|
+
const calendar = connectors.people.getCalendar({
|
|
134
|
+
subject: user.subject,
|
|
135
|
+
asOf: context.temporal.instant,
|
|
136
|
+
});
|
|
137
|
+
return { defaultTeamId: calendar.default_team_id, holidays: calendar.holidays };
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The callback records declarative fulfilment at author time; the shared runtime executes only read-only connector operations, validates the declared output, and freezes one snapshot for the whole invocation and any accepted interaction. Tools/resources/prompts read `context.temporal`, `context.ambient`, and `context.ambientStatus`. The embedded assistant receives the same snapshot in trusted platform context. Canonical TypeScript `server()` authoring emits an empty context declaration even when you omit the option, so MCP clients receive the reserved read-only `noodle_context` temporal tool with zero setup; add `server.context` only for custom defaults or ambient facts. Raw Core-v1 manifests activate the adapter only when that field is present. Never declare a TypeScript author tool named `noodle_context`. Keep ambient facts compact: the platform caps serialized JSON at 16 KiB, depth 8, and 128 entries per container, and rejects credential-shaped keys.
|
|
144
|
+
|
|
145
|
+
## Structured missing input
|
|
146
|
+
|
|
147
|
+
A tool authored with `ctx.elicit({ id, message, input })` produces `input_requested` when it reaches the missing value. The built-in and headless renderers consume the same advertised endpoints and interaction-event protocol: either renderer presents that event and, after the current turn stream completes, calls `respond(id, { action: "accept", content })`; decline/cancel stop the flow. Accepted content is schema-validated and completed steps are not rerun; invalid content returns `arg_invalid` and leaves the same request pending for correction. A chained request receives a fresh id and `tool_completed` appears only after the final answer. Every interactive flow must collect all elicited input before its first connector operation. Elicitation gathers an input; it does not approve a later write, which remains separately confirmation-gated. In a flow marked `confirm: true`, every eligible `input_requested` precedes `tool_proposed`; the final proposal reviews the original tool input, elicited values, and sole exact connector version/operation/resolved arguments. Accept is bound to that action and only then may execution start. Confirmable flows may contain at most one connector operation. Bidirectional MCP transports map missing input to standard form `elicitation/create`; an adapter that cannot carry that request fails before executing the tool. The same negotiated form capability carries a final affirmative confirmation and fails closed when unavailable. At the manifest/runtime boundary and in TypeScript action helpers, omitted or `false` preserves direct execution; action hints alone never gate. `annotations.action({ confirm: false })`, like omission, runs directly, while `annotations.action({ confirm: true })` explicitly enables confirmation.
|
|
148
|
+
|
|
117
149
|
## Verified session context (identity and claims)
|
|
118
150
|
|
|
119
151
|
The embedding developer defines what authenticated session context the assistant receives. One mechanism, three hops:
|
|
@@ -152,15 +184,17 @@ tool("greet", {
|
|
|
152
184
|
});
|
|
153
185
|
```
|
|
154
186
|
|
|
155
|
-
Manifest expressions use `${user.name}`, `${user.email}`, `${user.subject}`, `${user.claims.<key>}`. The model receives one platform identity line automatically: standard identity (name/email) whenever present, plus only the claims marked `exposeToModel: true` — so the assistant greets the actual user and can pass identity into tool arguments. `noodle check --target embedded-assistant` lists the declared claim contract.
|
|
187
|
+
Manifest expressions use `${user.name}`, `${user.email}`, `${user.subject}`, `${user.locale}`, `${user.timeZone}`, and `${user.claims.<key>}`. The model receives one platform identity line automatically: standard identity (name/email) whenever present, plus only the claims marked `exposeToModel: true` — so the assistant greets the actual user and can pass identity into tool arguments. `noodle check --target embedded-assistant` lists the declared claim contract.
|
|
156
188
|
|
|
157
|
-
Page `context` from the widget remains untrusted hint data; verified facts belong in `claims`,
|
|
189
|
+
Page `context` from the widget remains untrusted hint data; verified identity/authorization facts belong in `claims`, saved locale/time-zone choices belong in backend `preferences`, and live business facts belong in `server.context.ambient`. Validated preferences also reach fulfilments as `user.locale` and `user.timeZone`, so connectors format in the same verified zone the invocation snapshot uses.
|
|
190
|
+
|
|
191
|
+
To make the *downstream API call itself* run as the signed-in user (your API enforces its own per-user authorization instead of trusting a forwarded id), give the connector `auth.kind: "delegatedTokenExchange"` — the platform signs a verifiable assertion of this session identity and exchanges it at a token endpoint you implement. Assistant sessions carry the identity this needs; the full contract and a copyable endpoint implementation are in `references/authoring-workflow.md` ("Delegated downstream auth").
|
|
158
192
|
|
|
159
193
|
## The session response
|
|
160
194
|
|
|
161
|
-
The exchange returns the versioned Embedded Assistant v1 contract. `token`, `expiresAt`, and `endpoints.turns` / `endpoints.toolConfirmations` (absolute URLs) are always present; `configuration` is optional theming data. Forward the body unchanged;
|
|
195
|
+
The exchange returns the versioned Embedded Assistant v1 contract. `token`, `expiresAt`, and `endpoints.turns` / legacy `endpoints.toolConfirmations` (absolute URLs) are always present; current services add `endpoints.interactions` for accept/decline/cancel. `configuration` is optional theming data. Forward the body unchanged; browser clients choose the advertised endpoint. Do not rebuild, filter, or rewrite the response.
|
|
162
196
|
|
|
163
|
-
##
|
|
197
|
+
## Choose a browser renderer
|
|
164
198
|
|
|
165
199
|
Use the React wrapper in React applications:
|
|
166
200
|
|
|
@@ -174,19 +208,65 @@ Or import the package root once and mount `<noodle-assistant session-endpoint="/
|
|
|
174
208
|
|
|
175
209
|
The component renders a custom element and must mount client-side. In a Next.js App Router tree, put the mount in a `"use client"` component; from a server component or the Pages Router, load it with `next/dynamic` and `ssr: false`.
|
|
176
210
|
|
|
211
|
+
For a customer-owned renderer, use the DOM-free client. It keeps the session token in memory, streams the same typed events, and never registers a custom element:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { createAssistantClient } from "@noodleseed/assistant/client";
|
|
215
|
+
|
|
216
|
+
const assistant = createAssistantClient({
|
|
217
|
+
sessionEndpoint: "/api/assistant/session",
|
|
218
|
+
clientContext: () => ({
|
|
219
|
+
locale: navigator.language,
|
|
220
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
assistant.updateModelContext({
|
|
225
|
+
content: [{ type: 'text', text: 'The time-off form is mounted.' }],
|
|
226
|
+
structuredContent: { widget: { name: 'time-off', lifecycle: 'mounted' } },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
let pendingId: string | undefined;
|
|
230
|
+
let requestedSchema: unknown;
|
|
231
|
+
assistant.subscribe((event) => {
|
|
232
|
+
renderAssistantEvent(event);
|
|
233
|
+
if (event.event === 'view_available') {
|
|
234
|
+
renderRegisteredView(event.data.resourceUri, event.data.result);
|
|
235
|
+
}
|
|
236
|
+
if ((event.event === 'tool_proposed' || event.event === 'input_requested') && typeof event.data.id === 'string') {
|
|
237
|
+
pendingId = event.data.id;
|
|
238
|
+
requestedSchema = event.event === 'input_requested' ? event.data.requestedSchema : undefined;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await assistant.sendMessage("Book next Thursday and Friday off");
|
|
243
|
+
if (pendingId) {
|
|
244
|
+
const resolution = requestedSchema
|
|
245
|
+
? { action: 'accept' as const, content: await renderPortableForm(requestedSchema) }
|
|
246
|
+
: { action: 'accept' as const };
|
|
247
|
+
await assistant.respond(pendingId, resolution);
|
|
248
|
+
}
|
|
249
|
+
// The same pending id also accepts { action: 'decline' } or { action: 'cancel' }.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`view_available` means a completed tool has a linked MCP App view. It carries the call/interaction id, tool, `ui://` identity, optional title, and bounded/redacted public result; it does not claim that anything rendered. A headless customer renderer maps that identity to an already-trusted component. Never fetch the `ui://` URI or inject its HTML. The standard element likewise does not render it; it dispatches `assistant-view-available` with the same detail.
|
|
253
|
+
|
|
254
|
+
`clientContext` is recomputed for each turn. `updateContext(...)` changes separate untrusted page context for the next session exchange; call `resetSession()` when it must take effect immediately. `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. It is untrusted per-turn data, not conversation history or authorization input, and both 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, and 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.
|
|
255
|
+
|
|
177
256
|
## Toolchain requirements
|
|
178
257
|
|
|
179
258
|
- Node.js 20+ for `@noodleseed/assistant/server`.
|
|
180
259
|
- The package ships ESM and CommonJS with full export conditions; no bundler aliases, `transpilePackages`, or ambient type shims are needed. If resolution fails, the installed package version is outdated: update `@noodleseed/assistant` instead of adding workarounds.
|
|
181
|
-
- TypeScript `moduleResolution` `bundler` or `node16` recommended; classic `node` also resolves the `/react
|
|
260
|
+
- TypeScript `moduleResolution` `bundler` or `node16` recommended; classic `node` also resolves the `/client`, `/react`, and `/server` subpaths.
|
|
182
261
|
|
|
183
262
|
## Verify the boundary
|
|
184
263
|
|
|
185
264
|
- Signed-out session exchange returns `401`.
|
|
186
265
|
- The browser network/DOM/storage contains no client secret or model key.
|
|
187
266
|
- The local and production origins match `allowedOrigins` character-for-character.
|
|
188
|
-
-
|
|
189
|
-
- An expired turn re-exchanges once;
|
|
267
|
+
- At the manifest/runtime boundary and in TypeScript action helpers, only `confirm: true` enables confirmation; omitted or `false` preserves Core-v1 direct execution. Action hints alone never enforce approval; `annotations.action({ confirm: false })` is equivalent to omission.
|
|
268
|
+
- An expired turn re-exchanges once; interaction decisions never auto-retry. An explicit same-decision repeat returns the stored outcome without executing again.
|
|
269
|
+
- Accept, decline, and cancel are single-use. Only accept executes; the server ignores replacement tool arguments.
|
|
190
270
|
- Wrong-origin and malformed-origin requests fail closed.
|
|
191
271
|
|
|
192
272
|
## Troubleshooting: symptom to diagnosis
|
|
@@ -201,8 +281,13 @@ The component renders a custom element and must mount client-side. In a Next.js
|
|
|
201
281
|
| Session exchange returns 404 | `serviceUrl` points at the deployment MCP endpoint | Use the control-plane service URL printed by `noodle assistant clients create` |
|
|
202
282
|
| Session exchange returns 403 `origin is not allowed` | Request origin differs from `allowedOrigins` character-for-character | Align the exact scheme/host/port on both sides and redeploy |
|
|
203
283
|
| Hydration or `HTMLElement is not defined` errors | The component mounted during server rendering | Mount client-only (`"use client"` or `next/dynamic` with `ssr: false`) |
|
|
204
|
-
| A
|
|
284
|
+
| A tool runs without the expected confirmation | Its compiled annotations omit `confirm: true` or explicitly set `false` | Pass `{ confirm: true }` to the action helper; action hints alone never gate. `noodle check --target embedded-assistant` lists every confirm-gated tool |
|
|
205
285
|
| `${user.claims.<key>}` is empty | Claim not declared in `sessionClaims` (or key typo) — undeclared claims are dropped at exchange | Declare the key in `embeddedAssistant({ sessionClaims })` and redeploy |
|
|
206
286
|
| `${user.name}` is empty | Backend did not pass `user.name` to `createAssistantSession` | Pass the verified name from the authenticated backend session |
|
|
207
287
|
| The model does not know a claim you passed | Claim is tools-only | Mark it `exposeToModel: true` in `sessionClaims` |
|
|
208
|
-
|
|
|
288
|
+
| Relative dates use the wrong day or time zone | No verified user preference and the browser hint is missing/stale | Pass saved `preferences` from the backend; provide a fresh per-turn `clientContext` in a headless renderer |
|
|
289
|
+
| The model invents a team/holiday after context lookup fails | The ambient provider returned invalid data or its read-only connector failed (`ambientStatus: unavailable`) | Fix the provider/connector; treat unavailable ambient facts as missing, never prompt instructions |
|
|
290
|
+
| Decline/cancel reports `unsupported_service` | The session came from a legacy service with no `endpoints.interactions` | Upgrade the service; legacy `toolConfirmations` supports accept only |
|
|
291
|
+
| Behavior does not change after `noodle deploy` | Outdated platform: before the 2026-07 fix, clients were pinned to their creation-time deployment | Update the platform; sessions now follow the tenant's active deployment |
|
|
292
|
+
| A delegated connector tool fails with `delegated token exchange requires a verified customer caller` | The calling surface has no verified customer identity (or an old session minted before the platform carried the resource audience) | Verify `customerAuth` is configured and the backend passes the verified `user` to `createAssistantSession`; re-mint the session |
|
|
293
|
+
| Deploy fails with `unsupported_delegated_provider` | `delegatedOAuth.provider` only supports the managed `firebase`/`microsoft` bridges | Use `auth.kind: "delegatedTokenExchange"` for your own token endpoint (see authoring-workflow.md) |
|
|
@@ -14,12 +14,12 @@ Paths are relative to this skill directory. Assets (images/fonts) are omitted fr
|
|
|
14
14
|
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. | `examples/acme-discovery/src/server.ts` + `design/` |
|
|
15
15
|
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. | `examples/acme-tasks/src/server.ts` + `design/` |
|
|
16
16
|
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). | `examples/acme-bistro/src/server.ts` + `design/` |
|
|
17
|
+
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. | `examples/customer-auth/src/server.ts` |
|
|
17
18
|
|
|
18
19
|
## In the repository only — `examples/<name>/` on GitHub
|
|
19
20
|
|
|
20
21
|
| Example | Use when |
|
|
21
22
|
| :-- | :-- |
|
|
22
|
-
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. |
|
|
23
23
|
| `stateful-draft` | Durable, caller-scoped widget state handles with optimistic revisions. |
|
|
24
24
|
| `perplexity` | A real SaaS API with bearer auth and a managed `secret`. |
|
|
25
25
|
| `bitcoin` | API-key HTTP connector, custom auth header, and compute normalization. |
|
|
@@ -127,7 +127,7 @@ prompt('summarize_ticket', {
|
|
|
127
127
|
|
|
128
128
|
### Non-trivial tool: ctx connectors, annotations, visibility, async
|
|
129
129
|
|
|
130
|
-
`ctx` is `{ input, user, connectors }`. Bind connectors with `use` on the server, then call one inside `fulfil` to record a step. `annotations.readOnly()`
|
|
130
|
+
`ctx` is `{ input, user, connectors }`. Bind connectors with `use` on the server, then call one inside `fulfil` to record a step. `annotations.readOnly()` declares a closed-world safe read. TypeScript action helpers enforce confirmation only with `{ confirm: true }`; omitted or `false` executes directly, and action/destructive/open-world hints alone never enable the gate. `visibility` defaults to `['model', 'app']` — set `['app']` to hide a helper from the model. `fulfil` may be `async` (the compiler awaits it while recording).
|
|
131
131
|
|
|
132
132
|
```ts
|
|
133
133
|
import { annotations, connector, server, tool, z } from '@noodleseed/one';
|
|
@@ -169,7 +169,7 @@ export default server('support', { title: 'Support', version: '1.0.0', use: { cr
|
|
|
169
169
|
description: 'Echo text back.',
|
|
170
170
|
input: z.object({ text: z.string() }),
|
|
171
171
|
output: z.object({ echo: z.string() }),
|
|
172
|
-
annotations: annotations.action(), //
|
|
172
|
+
annotations: annotations.action(), // world-affecting hint; add { confirm: true } to gate
|
|
173
173
|
// fulfil may be async — the compiler awaits it while recording the flow.
|
|
174
174
|
fulfil: async ({ input }) => ({ echo: input.text }),
|
|
175
175
|
}),
|
|
@@ -38,10 +38,12 @@ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@
|
|
|
38
38
|
| `useRequestDisplayMode` | Request a host-mediated layout change such as fullscreen; treat it as best-effort and keep inline rendering useful. |
|
|
39
39
|
| `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
|
|
40
40
|
| `useSendFollowUpMessage` | Send a follow-up prompt to the model from a user interaction: `send({ prompt })` — trigger only from an explicit user action. |
|
|
41
|
+
| `useUpdateModelContext` | Publish one compact, cohesive author-selected text/structured snapshot through the standard MCP Apps model-context channel; each call replaces the prior snapshot, so include every still-relevant field and check `useLayout().supports?.modelContext` first. |
|
|
42
|
+
| `useWidgetLifecycle` | Calling the hook auto-publishes `mounted` and listens for host `cancelled`/`dismissed`; use its publisher for author-owned `submitted` or app milestones, include a complete safe replacement snapshot, and pair explicit submit/cancel with `useSendFollowUpMessage` when an immediate reply is wanted. |
|
|
41
43
|
| `useAppFlow` | Manage named widget views with persisted params and back-stack state: `const flow = useAppFlow({ initialView, views })`. |
|
|
42
44
|
| `useHandoff` | Open server-created HTTP(S) handoff URLs through the host with status/error state; domain policy still comes from `handoff.allowedDomains`. |
|
|
43
45
|
|
|
44
|
-
Bind interactive elements to tools (`useCallTool("place_order")`), drive named views with `useAppFlow(...)`, open server-created handoffs with `useHandoff()`, and
|
|
46
|
+
Bind interactive elements to tools (`useCallTool("place_order")`), drive named views with `useAppFlow(...)`, open server-created handoffs with `useHandoff()`, and publish one compact, safe, cohesive snapshot with `useUpdateModelContext()` when `useLayout().supports?.modelContext` is true. Every model-context or lifecycle publication replaces the prior snapshot rather than merging fields, so include everything the model should still know. Calling `useWidgetLifecycle("name")` automatically publishes `mounted`, listens for host `cancelled` and `dismissed`, and returns a publisher for author-owned `submitted` or app-specific milestones; `mounted` is not proof that the host presented pixels. Both hooks use the standard MCP Apps model-context channel, not a host-specific API. These updates affect future model context but do not start a model turn. When an explicit user submit/cancel should receive an immediate reply, also call `useSendFollowUpMessage()` from that user action. `data-llm` may remain a DOM inspection hint, but it is not the bidirectional model-state contract. Use `createViewStore("key", initial)` for multi-component widget state such as carts, filters, or drafts. Use the domain-neutral React components from `@noodleseed/one/react` (`AppShell`, `ShellNav`, `ViewStack`, `AsyncBoundary`, `ActionBar`, `Field`, `QuantityStepper`, `ChoiceGroup`, `HandoffButton`, and related state components) for rich apps before inventing local shell/control scaffolding. Adapt to the host with `useLayout()` — style for both `theme` values, and keep the inline `displayMode` compact (content fits the space; no internal scrolling). Trigger `useOpenExternal()`, `useHandoff()`, and `useSendFollowUpMessage()` only from explicit user actions. A raw `html` escape hatch exists for self-contained widgets (declarative `data-bind`/`data-action`; no inline `<script>`).
|
|
45
47
|
|
|
46
48
|
## Worked widget recipe
|
|
47
49
|
|
|
@@ -49,14 +51,15 @@ Minimal, complete, and compile-verified — `noodle validate` bundles the view a
|
|
|
49
51
|
|
|
50
52
|
### 1. The view component
|
|
51
53
|
|
|
52
|
-
Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks: read the tool result with `useToolInfo`, call a widget-only helper with `useCallTool`, keep local UI state that survives re-render with `useViewState`, open an allowlisted link with `useOpenExternal`, and
|
|
54
|
+
Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks: read the tool result with `useToolInfo`, call a widget-only helper with `useCallTool`, keep local UI state that survives re-render with `useViewState`, open an allowlisted link with `useOpenExternal`, and publish cohesive model-context snapshots plus author-owned lifecycle milestones.
|
|
53
55
|
|
|
54
56
|
```tsx
|
|
57
|
+
import { useEffect } from 'react';
|
|
55
58
|
import type { ServerDefinition } from '@noodleseed/one';
|
|
56
59
|
import { Action, ActionBar, AsyncBoundary, Feedback, Field, Flow, Frame, Region, Select, generateHelpers } from '@noodleseed/one/react';
|
|
57
60
|
|
|
58
61
|
// One call wires the typed host bridge; destructure only the hooks this view uses.
|
|
59
|
-
const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
|
|
62
|
+
const { useToolInfo, useCallTool, useViewState, useLayout, useOpenExternal, useSendFollowUpMessage, useUpdateModelContext, useWidgetLifecycle } =
|
|
60
63
|
generateHelpers<ServerDefinition>();
|
|
61
64
|
|
|
62
65
|
type OrderResult = {
|
|
@@ -69,14 +72,36 @@ type OrderResult = {
|
|
|
69
72
|
export default function OrderStatus() {
|
|
70
73
|
const shown = useToolInfo('show_order').structuredContent as OrderResult | undefined;
|
|
71
74
|
const placeOrder = useCallTool('place_order'); // calls the widget-only helper tool
|
|
75
|
+
const { supports } = useLayout();
|
|
72
76
|
const openExternal = useOpenExternal();
|
|
77
|
+
const sendFollowUpMessage = useSendFollowUpMessage();
|
|
78
|
+
const updateModelContext = useUpdateModelContext();
|
|
79
|
+
const publishLifecycle = useWidgetLifecycle('pickup-order');
|
|
73
80
|
const [item, setItem] = useViewState('item', shown?.item ?? 'falafel_wrap'); // survives re-render
|
|
74
81
|
const confirmed = placeOrder.data?.structuredContent as { readonly status?: string } | undefined;
|
|
75
82
|
const total = shown?.total ?? 0;
|
|
76
83
|
const checkoutUrl = shown?.checkoutUrl ?? '';
|
|
77
84
|
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!supports?.modelContext) return;
|
|
87
|
+
void updateModelContext({
|
|
88
|
+
content: [{ type: 'text', text: `Pickup order: ${item}, total ${total}` }],
|
|
89
|
+
structuredContent: { widget: { name: 'pickup-order', lifecycle: 'active' }, order: { item, total } },
|
|
90
|
+
});
|
|
91
|
+
}, [item, supports?.modelContext, total, updateModelContext]);
|
|
92
|
+
|
|
93
|
+
async function submitOrder() {
|
|
94
|
+
await placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item });
|
|
95
|
+
if (supports?.modelContext) {
|
|
96
|
+
await publishLifecycle('submitted', { surface: 'order', item, total, status: 'submitted' });
|
|
97
|
+
}
|
|
98
|
+
if (supports?.followUpMessage) {
|
|
99
|
+
await sendFollowUpMessage({ prompt: `The pickup order was submitted for ${item}. Confirm the result.` });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
return (
|
|
79
|
-
// data-llm
|
|
104
|
+
// data-llm is an inspection hint; useUpdateModelContext above is the model-visible channel.
|
|
80
105
|
<Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
|
|
81
106
|
<Flow variant="stack">
|
|
82
107
|
<AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
|
|
@@ -92,7 +117,7 @@ export default function OrderStatus() {
|
|
|
92
117
|
{confirmed?.status ? <Feedback status="success">{confirmed.status}</Feedback> : null}
|
|
93
118
|
<ActionBar>
|
|
94
119
|
<Action variant="primary" pending={placeOrder.isPending} pendingLabel="Placing…"
|
|
95
|
-
onClick={
|
|
120
|
+
onClick={submitOrder}
|
|
96
121
|
>
|
|
97
122
|
Place order
|
|
98
123
|
</Action>
|
|
@@ -160,8 +185,8 @@ export default server(
|
|
|
160
185
|
tool('place_order', {
|
|
161
186
|
visibility: ['app'],
|
|
162
187
|
description: 'Place a pickup order from the widget.',
|
|
163
|
-
//
|
|
164
|
-
annotations: annotations.action(),
|
|
188
|
+
// Widget-mediated direct action: explicit false is equivalent to omitting confirmation.
|
|
189
|
+
annotations: annotations.action({ confirm: false }),
|
|
165
190
|
input: z.object({ customer: z.string().default('Guest'), item }),
|
|
166
191
|
output: z.object({ status: z.string(), item: z.string(), checkoutUrl: z.string() }),
|
|
167
192
|
fulfil: ({ input }) => ({
|