sygnal 6.3.0 → 6.4.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/CHANGELOG.md +51 -0
- package/dist/astro/index.cjs.js +69 -3
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +69 -3
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +47 -8
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +47 -8
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/mcp-apps.md +59 -4
- package/dist/index.cjs.js +9 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.esm.js +9 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +69 -3
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +69 -3
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/extra/diagnostics/checks/dom.ts +10 -2
- package/src/extra/diagnostics/checks/next.ts +17 -1
- package/src/extra/diagnostics/checks/persist.ts +4 -2
- package/src/extra/diagnostics/checks/wiring.ts +14 -6
- package/src/extra/diagnostics/codes.ts +5 -0
- package/src/extra/pwa.ts +3 -1
- package/src/extra/reducers.ts +10 -4
- package/src/index.d.ts +9 -2
- package/src/vite/plugin.ts +63 -2
package/dist/guide/mcp-apps.md
CHANGED
|
@@ -48,10 +48,19 @@ App.model = {
|
|
|
48
48
|
STATE: (state) => ({ ...state, status: 'loading' }),
|
|
49
49
|
MCP: (state) => ({ callTool: 'get_forecast', args: { city: state.city }, ok: 'RESULT' }),
|
|
50
50
|
},
|
|
51
|
-
// tell the model what the user picked: it reads it
|
|
51
|
+
// tell the model what the user picked: store it on the server, where the model reads it with a
|
|
52
|
+
// tool, then send it as model context too (see Model context below)
|
|
52
53
|
PICK: {
|
|
53
54
|
STATE: (state, date) => ({ ...state, selected: date }),
|
|
54
|
-
MCP: (state, date) => ({
|
|
55
|
+
MCP: (state, date) => ({ callTool: 'select_day', args: { city: state.city, day: state.days.find((d) => d.date === date) }, ok: 'PICKED' }),
|
|
56
|
+
},
|
|
57
|
+
PICKED: {
|
|
58
|
+
MCP: (state) => ({
|
|
59
|
+
updateModelContext: {
|
|
60
|
+
content: `The user selected ${state.selected} in the ${state.city} forecast card.`,
|
|
61
|
+
structuredContent: { city: state.city, selectedDay: state.days.find((d) => d.date === state.selected) },
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
55
64
|
},
|
|
56
65
|
// post a message to the conversation, as the user
|
|
57
66
|
ASK: { MCP: (state) => ({ message: `What should I pack for ${state.city} on ${state.selected}?` }) },
|
|
@@ -92,7 +101,7 @@ The latest tool input, tool result, host context and `host` are replayed to a co
|
|
|
92
101
|
| Value | Does |
|
|
93
102
|
|---|---|
|
|
94
103
|
| `{ callTool: 'name', args, ok, error }` | Calls a tool on the MCP server through the host. `ok` gets the `CallToolResult`; `error` gets `{ error, code?, result?, request }` (a JSON-RPC error, or a result with `isError: true`) |
|
|
95
|
-
| `{ updateModelContext: value }` | Tells the model something, for its next turn: a string (one text block), content blocks,
|
|
104
|
+
| `{ updateModelContext: value }` | Tells the model something, for its next turn: a string (one text block), content blocks, an object (`structuredContent`), or `{ content, structuredContent }`. The host decides whether the model sees it: see [Model context](#model-context) |
|
|
96
105
|
| `{ message: 'text' }` | Posts a message to the conversation as the user (a string or content blocks) |
|
|
97
106
|
| `{ openLink: 'https://…' }` | Asks the host to open a link |
|
|
98
107
|
| `{ displayMode: 'fullscreen' }` | Asks for `'inline'`, `'fullscreen'` or `'pip'`; `ok` gets `{ mode }`, the mode the host chose |
|
|
@@ -155,6 +164,45 @@ Export.model = {
|
|
|
155
164
|
|
|
156
165
|
**Theme and size.** Follow `host-context-changed` for the host's `theme` (and its `styles.variables`, CSS custom properties the host suggests), so the view matches the conversation around it. Keep the view compact for `inline` mode; offer full screen with `{ displayMode: 'fullscreen' }` when `availableDisplayModes` includes it.
|
|
157
166
|
|
|
167
|
+
### Model context
|
|
168
|
+
|
|
169
|
+
A view has three ways to tell the model something, and hosts handle them differently:
|
|
170
|
+
|
|
171
|
+
- `{ message }` posts a message to the conversation as the user. The model sees it and answers it, in every host that supports `message`. Use it when the user's click is a question (the starter's **Ask about this day**).
|
|
172
|
+
- `{ updateModelContext }` gives the host context for the model's **next** turn, and the host decides what to do with it. The spec says a host *should* pass it on, not how. Cursor adds it to the next user message; claude.ai keeps it and lets the model fetch it with a built-in tool ("Reading widget context"), and so does Claude Desktop's Code tab (`read_widget_context`, which the host points the model to); Claude Desktop's chat (tested October 2026) accepts it, but its model has no way to read it.
|
|
173
|
+
- **A tool on your server.** The view saves what the user did with a tool only the view may call, and the model reads it with an ordinary tool. This reaches the model in every host, because every host offers the server's tools.
|
|
174
|
+
|
|
175
|
+
The starter does the last two. When the user picks a day, the view calls `select_day`, then sends the same selection as model context:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
// server/server.js
|
|
179
|
+
let selection = null // one per process: the stdio server runs one per host connection
|
|
180
|
+
|
|
181
|
+
// only the view can call it: visibility ['app'] keeps it out of the model's tool list
|
|
182
|
+
server.registerTool('select_day', {
|
|
183
|
+
description: 'Records the day the user selected in the forecast card.',
|
|
184
|
+
inputSchema: { city: z.string(), day: z.object({ date: z.string(), sky: z.string(), high: z.number(), low: z.number() }) },
|
|
185
|
+
_meta: { ui: { resourceUri: VIEW_URI, visibility: ['app'] } },
|
|
186
|
+
}, async ({ city, day }) => {
|
|
187
|
+
selection = { city, day }
|
|
188
|
+
return { content: [{ type: 'text', text: `Selected ${day.date} in ${city}` }], structuredContent: selection }
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// the model's way to read it; get_forecast's description says to call it
|
|
192
|
+
server.registerTool('get_selected_day', {
|
|
193
|
+
description: 'The day the user selected in the forecast card shown by get_forecast, if any.',
|
|
194
|
+
inputSchema: {},
|
|
195
|
+
}, async () => ({
|
|
196
|
+
content: [{ type: 'text', text: selection ? `The user selected ${selection.day.date} in ${selection.city}.` : 'The user has not selected a day.' }],
|
|
197
|
+
}))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The stdio server runs once per host connection, so one variable holds one user's selection. A server many users share (Streamable HTTP behind a public URL) has to keep it per user or session.
|
|
201
|
+
|
|
202
|
+
Send `updateModelContext` with a text `content`: hosts list the content types they take (`MCP.select('host')` gives `hostCapabilities.updateModelContext`, Claude's is `{ text: {}, image: {} }`), and one that doesn't list `structuredContent` may drop an update that only has it.
|
|
203
|
+
|
|
204
|
+
A host may also keep a tool's view (the `ui://` resource) and the tool list for as long as the server stays connected: Claude Desktop fetches them once per connection. After rebuilding the view or changing a tool, reconnect the server (restart the host, or turn the server off and on) before trying it again.
|
|
205
|
+
|
|
158
206
|
## The view's own tools
|
|
159
207
|
|
|
160
208
|
The model sees the server's tools. With `tools: agentTools`, the view also offers its components' [`agent` declarations](./agent.md) to the host as tools of its own, so the model can operate the view while it is shown: select a day, change units, open a detail. Calls go through the same [call rules](./agent.md#the-call-rules) as any agent's.
|
|
@@ -263,7 +311,14 @@ it('shows the result, refreshes, and tells the model what the user picked', asyn
|
|
|
263
311
|
|
|
264
312
|
t.simulateEvent('.day', 'click', { data: { date: 'Tue' } })
|
|
265
313
|
await t.waitForState((s) => s.selected === 'Tue')
|
|
266
|
-
expect(t.requests('MCP').at(-1)).toEqual({
|
|
314
|
+
expect(t.requests('MCP').at(-1)).toEqual({ callTool: 'select_day', args: { city: 'Oslo', day: days[1] }, ok: 'PICKED' })
|
|
315
|
+
await t.respond('MCP', { content: [], structuredContent: { city: 'Oslo', day: days[1] } }, 'PICKED')
|
|
316
|
+
expect(t.requests('MCP').at(-1)).toEqual({
|
|
317
|
+
updateModelContext: {
|
|
318
|
+
content: 'The user selected Tue in the Oslo forecast card.',
|
|
319
|
+
structuredContent: { city: 'Oslo', selectedDay: days[1] },
|
|
320
|
+
},
|
|
321
|
+
})
|
|
267
322
|
})
|
|
268
323
|
```
|
|
269
324
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -785,6 +785,8 @@ const DEV_CODE_SEVERITY = {
|
|
|
785
785
|
SYG425: 'warn',
|
|
786
786
|
// PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
|
|
787
787
|
SYG426: 'warn',
|
|
788
|
+
// checks/next.ts: CHILD.select(undefined), usually a missing export
|
|
789
|
+
SYG427: 'error',
|
|
788
790
|
// PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
|
|
789
791
|
SYG453: 'warn',
|
|
790
792
|
SYG612: 'error',
|
|
@@ -17460,10 +17462,14 @@ function toggle(field) {
|
|
|
17460
17462
|
return (state) => ({ ...state, [field]: !state[field] });
|
|
17461
17463
|
}
|
|
17462
17464
|
// event(type, payload?) → EVENTS sink fn for the object form:
|
|
17463
|
-
// `ACTION: { EVENTS: event('TYPE', (state, data) => payload) }` (static payloads allowed)
|
|
17465
|
+
// `ACTION: { EVENTS: event('TYPE', (state, data) => payload) }` (static payloads allowed).
|
|
17466
|
+
// A payload function that returns ABORT sends nothing, as ABORT does from any other sink.
|
|
17464
17467
|
function event(type, payload) {
|
|
17465
17468
|
return typeof payload === 'function'
|
|
17466
|
-
? (state, data, next, props) =>
|
|
17469
|
+
? (state, data, next, props) => {
|
|
17470
|
+
const d = payload(state, data, next, props);
|
|
17471
|
+
return isAbort(d) ? ABORT : { type, data: d };
|
|
17472
|
+
}
|
|
17467
17473
|
: () => ({ type, data: payload });
|
|
17468
17474
|
}
|
|
17469
17475
|
// ── emit() ─────────────────────────────────────────────────────────
|
|
@@ -17501,7 +17507,7 @@ function trackWorker(worker, events) {
|
|
|
17501
17507
|
function makeServiceWorkerDriver(scriptUrl, options = {}) {
|
|
17502
17508
|
return function serviceWorkerDriver(sink$) {
|
|
17503
17509
|
const events = new EventTarget();
|
|
17504
|
-
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
17510
|
+
if (options.enabled !== false && typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
17505
17511
|
navigator.serviceWorker
|
|
17506
17512
|
.register(scriptUrl, { scope: options.scope })
|
|
17507
17513
|
.then((reg) => {
|