sygnal 6.2.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +3 -1
  3. package/dist/ai.cjs.js +36 -0
  4. package/dist/ai.cjs.js.map +1 -1
  5. package/dist/ai.esm.js +1 -1
  6. package/dist/astro/index.cjs.js +69 -3
  7. package/dist/astro/index.cjs.js.map +1 -1
  8. package/dist/astro/index.mjs +69 -3
  9. package/dist/astro/index.mjs.map +1 -1
  10. package/dist/astro/server.cjs.js +3 -3
  11. package/dist/astro/server.cjs.js.map +1 -1
  12. package/dist/astro/server.mjs +3 -3
  13. package/dist/astro/server.mjs.map +1 -1
  14. package/dist/diagnostics.cjs.js +211 -11
  15. package/dist/diagnostics.cjs.js.map +1 -1
  16. package/dist/diagnostics.esm.js +211 -11
  17. package/dist/diagnostics.esm.js.map +1 -1
  18. package/dist/guide/agent.md +7 -1
  19. package/dist/guide/ai-cards.md +319 -0
  20. package/dist/guide/ai-chat.md +4 -0
  21. package/dist/guide/ai-compose.md +881 -0
  22. package/dist/guide/ai-decisions.md +16 -0
  23. package/dist/guide/http.md +1 -0
  24. package/dist/guide/mcp-apps.md +59 -4
  25. package/dist/index.cjs.js +5704 -350
  26. package/dist/index.cjs.js.map +1 -1
  27. package/dist/index.d.ts +647 -10
  28. package/dist/index.esm.js +5694 -350
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/sygnal.min.js +1 -1
  31. package/dist/sygnal.min.js.map +1 -1
  32. package/dist/vike/config/package.json +1 -1
  33. package/dist/vike/onRenderClient.cjs.js +3 -2
  34. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  35. package/dist/vike/onRenderClient.mjs +3 -2
  36. package/dist/vike/onRenderClient.mjs.map +1 -1
  37. package/dist/vite/plugin.cjs.js +69 -3
  38. package/dist/vite/plugin.cjs.js.map +1 -1
  39. package/dist/vite/plugin.mjs +69 -3
  40. package/dist/vite/plugin.mjs.map +1 -1
  41. package/llms.txt +7 -5
  42. package/package.json +2 -1
  43. package/src/ai.d.ts +642 -7
  44. package/src/ai.ts +6 -0
  45. package/src/collection.ts +2 -1
  46. package/src/core/instance.ts +6 -3
  47. package/src/core/markers/lazy.ts +7 -3
  48. package/src/core/markers/portal.ts +5 -2
  49. package/src/core/markers/suspense.ts +3 -2
  50. package/src/core/markers/transition.ts +6 -3
  51. package/src/core/registry.ts +9 -5
  52. package/src/cycle/dom/IsolateModule.ts +25 -22
  53. package/src/extra/ai/chat/behavior.ts +354 -81
  54. package/src/extra/ai/chat/driver.ts +7 -4
  55. package/src/extra/ai/compose/agent.ts +55 -0
  56. package/src/extra/ai/compose/ask.ts +147 -0
  57. package/src/extra/ai/compose/behavior.ts +183 -0
  58. package/src/extra/ai/compose/catalog.ts +211 -0
  59. package/src/extra/ai/compose/composed.ts +61 -0
  60. package/src/extra/ai/compose/decisions.ts +1649 -0
  61. package/src/extra/ai/compose/diag.ts +33 -0
  62. package/src/extra/ai/compose/index.ts +23 -0
  63. package/src/extra/ai/compose/layout.ts +441 -0
  64. package/src/extra/ai/compose/llm.ts +1120 -0
  65. package/src/extra/ai/compose/render.ts +159 -0
  66. package/src/extra/ai/compose/testing.ts +71 -0
  67. package/src/extra/ai/compose/types.ts +159 -0
  68. package/src/extra/ai/decide.ts +97 -1
  69. package/src/extra/ai/index.ts +6 -1
  70. package/src/extra/diagnostics/checks/dom.ts +10 -2
  71. package/src/extra/diagnostics/checks/next.ts +103 -3
  72. package/src/extra/diagnostics/checks/persist.ts +4 -2
  73. package/src/extra/diagnostics/checks/wiring.ts +14 -6
  74. package/src/extra/diagnostics/codes.ts +59 -0
  75. package/src/extra/fetchDriver.ts +58 -27
  76. package/src/extra/lens.ts +15 -0
  77. package/src/extra/pwa.ts +3 -1
  78. package/src/extra/reducers.ts +10 -4
  79. package/src/extra/ssr.ts +3 -3
  80. package/src/extra/testing.ts +2 -0
  81. package/src/extra/widget.ts +3 -3
  82. package/src/index.d.ts +50 -2
  83. package/src/index.ts +8 -0
  84. package/src/lazy.ts +3 -2
  85. package/src/portal.ts +5 -2
  86. package/src/pragma/index.ts +2 -1
  87. package/src/suspense.ts +5 -2
  88. package/src/switchable.ts +2 -1
  89. package/src/transition.ts +5 -2
  90. package/src/vike/onRenderClient.ts +3 -2
  91. package/src/vite/plugin.ts +63 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,118 @@
2
2
 
3
3
  All notable changes to Sygnal are listed here. Versions follow [semantic versioning](https://semver.org). Releases before 5.4.0 are described in the [GitHub releases](https://github.com/tpresley/sygnal/releases) and tags.
4
4
 
5
+ ## 6.4.0 — 2026-10-14
6
+
7
+ Fixes for issues an agent hit while building a real app on 6.3.0, and a step-by-step chat tutorial in the AI showcase. Additive: an `event()` payload function may now return `ABORT`, `makeServiceWorkerDriver` takes `enabled`, and a new diagnostic code (SYG427) reports a `CHILD.select()` of something that isn't a component. Dev-time checks get fewer false positives (SYG103, SYG223).
8
+
9
+ **Measured impact.** Size: the kanban gate is (a) 42,516 B with `nativeGlobalThis: false` (was 42,504 B; budget 42,700 B) and (b) 38,497 B by default (was 38,485 B): `event()`'s `ABORT` check adds 12 B. The SYG103 view words are dev-server only (never in `vite build`); SYG427's runtime check is in the dev diagnostics entry (0 production bytes).
10
+
11
+ **Companion packages.**
12
+ - **`sygnal-check` 0.8.0:** new SYG427 (error: a component imported from a module that doesn't export it), no false SYG223 for a `uses` slice in `persist({ pick })`, and SYG609's fix names `makeServiceWorkerDriver` for an `SW` sink. Its caret range doesn't reach 0.8.0 from `^0.7.0`, so update the dev dependency.
13
+ - **`create-sygnal-app` 2.6.0:** every template depends on `sygnal` ^6.4.0 and `sygnal-check` ^0.8.0.
14
+ - **VS Code extension 0.5.0:** bundles `sygnal-check` 0.8.0. The extension itself doesn't change.
15
+ - **Claude Code plugin 0.1.3:** the `sygnal-dev` skill says that an `event()` payload function returns `ABORT` to send nothing.
16
+
17
+ ### Added
18
+
19
+ - **`event()` can send nothing.** A payload function that returns `ABORT` sends no event, as `ABORT` does from any other sink: `EVENTS: event('SAVED', (state) => (state.dirty ? { id: state.id } : ABORT))`. Before, the event went out with `ABORT` as its data, and strict mode (which rules out the raw `EVENTS: (state) => …` form) left no way to skip one; apps split actions or sent placeholder events instead. Typed: the payload function may return `ABORT`. `emit()` gets it too.
20
+ - **`makeServiceWorkerDriver(url, { enabled })`.** `enabled: false` registers no service worker, ignores commands and emits nothing, so an app keeps its `SW` wiring on a dev server, where a worker must not run: `makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD })`. Before, leaving the driver out in development logged SYG609 on every load, and the fix was a hand-written no-op driver.
21
+ - **SYG427 (error): `CHILD.select()` of something that is not a component.** A component imported by a name its module doesn't export is `undefined` under Vitest (the browser refuses to load the module), so `CHILD.select(Child)` never emitted and a test that never rendered `<Child />` passed with no diagnostic. The dev diagnostics now report it when the parent is created, so `expectNoDiagnostics()` fails in every test of the parent; `sygnal-check` reports a component imported from a relative module that doesn't export it, used as a JSX tag, in `CHILD.select()` or as a Collection's `of`.
22
+ - **`examples/ai-showcase`: a Building Chat tab.** The page is now two tabs: Features (the ten sections) and Building Chat, one chat built up in eight steps on one model (Kimi K3 on Kindo): streaming; stop, retry and clear; the model's reasoning and a token tally; follow-up suggestions from a structured-output request; `persist()`; an `agent` declaration the model reads (an answer style) and acts on (a shopping list, with approval before clearing it); and a client tool answered with a click. Each step is a complete component, and Show code marks the lines new since the step before. Example chips fill in the prompt without sending it.
23
+
24
+ ### Fixed
25
+
26
+ - **SYG103 no longer warns about markup drawn by a view helper.** The dev check that takes a not-yet-rendered selector as conditionally rendered read only the view function's source, so a button drawn by `panel()` (called from the view, in the same file or imported) counted as missing and warned after a few renders; the only way out was a component per branch. In dev, `sygnal/vite` now registers the words of a component module's string literals and of its relative imports, and the check reads them too. Strings that are selectors or paths are left out, so a typo in the intent's own selector is still reported. Without the plugin the check works as before.
27
+ - **SYG223 no longer flags a behavior's slice.** `persist({ pick: ['chat'] })` on a component with `uses = { chat: chat() }` warned that `chat` isn't a key of `initialState` (dev diagnostics and `sygnal-check`), but a `uses` slice is state and is saved; both checkers now count `uses` keys.
28
+ - **SYG609's fix names the right driver.** An `SW` sink now gets `makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD })` instead of `makeFetchDriver()`, and a `WS` sink `makeSocketDriver()`, without the URL argument the fix text gave it (the socket driver takes none; connections come from the `connections` static). Runtime and `sygnal-check`.
29
+
30
+ ## create-sygnal-app 2.5.0 — 2026-10-13
31
+
32
+ The `mcp-app` starter, new projects on sygnal-check 0.7.0, docs and examples; `sygnal` itself is unchanged (6.3.0). Testing the starter in Claude Desktop showed that a view's `updateModelContext` never reaches the model in its chat, so the starter now tells the model what the user picked through a server tool.
33
+
34
+ ### Changed
35
+
36
+ - **`create-sygnal-app`: the `mcp-app` template (JS and TS) tells the model what the user picked through the server.** Hosts differ in what they do with `ui/update-model-context`: Cursor adds it to the next turn, claude.ai and Claude Desktop's Code tab let the model fetch it with a built-in tool, and Claude Desktop's chat accepts it (it lists `{ text, image }`) but its model never sees it (tested 2026-10-12). When the user picks a day, the view now calls `select_day`, a tool only the view can call (`_meta.ui.visibility: ['app']`), and the model reads the selection with `get_selected_day`, which `get_forecast`'s description names; the view still sends the selection as model context, now as text plus `structuredContent` (a host that doesn't list `structuredContent` may drop an update with only that). The README and `AGENTS.md` explain the pattern, and the README says to reconnect the server after a rebuild (Claude Desktop keeps the view and the tool list per connection).
37
+ - **`create-sygnal-app`: new projects get `sygnal-check` ^0.7.0** (was ^0.6.0; a caret range on 0.x doesn't cross minors), so the editor highlights of sygnal-check 0.7.0 (below) work in them from the start. `sygnal` stays ^6.3.0.
38
+
39
+ ### Added
40
+
41
+ - **Docs: Native AI Support**, a section above the Guide with seven narrative pages (overview, streaming chat, decision models, UI composed by a model, agents in your app, the agentic web, built for coding agents) that link to the guides, recipes and API reference. The home page lists the AI card right after Pure Components.
42
+ - **Docs: Model context** in [MCP Apps](https://sygnal.js.org/guide/mcp-apps/#model-context): `message` vs `updateModelContext` vs a server tool, how hosts handle each, and the selection tools' server code.
43
+ - **`examples/ai-showcase`**: one page with ten sections, each a small Sygnal app showing a part of `sygnal/ai` against real models (Ollama, Kindo through its Open Responses, Chat Completions and Messages endpoints, TypeSafe Jev, Chrome's built-in model): any model with the same component, structured output, tools, decision models, the command bar, an assistant that operates the app, composed UI, cards in a chat, WebMCP, and an MCP App. Each section has its source behind Show code. Provider keys stay on the dev server (a Keys dialog or key files outside the repository; `apply: 'serve'` routes that refuse cross-site requests).
44
+ - **`examples/mcp-kindo-agents`**: an MCP App that runs Kindo agents from a conversation: the model fills in the inputs, the card starts the run, follows it through a server-side long poll (step evaluations, a live elapsed time) and hands the result back to the model (`get_agent_run`).
45
+ - **`examples/mcp-app`**: the `mcp-app` starter linked to this checkout (part of `npm run test:examples`), with `scripts/add-to-claude-desktop.sh`, which adds the server to Claude Desktop while it is quit (Claude Desktop rewrites `claude_desktop_config.json` while it runs, dropping edits made then).
46
+
47
+ ## sygnal-check 0.7.0, VS Code extension 0.4.0 — 2026-10-13
48
+
49
+ Highlights of Sygnal's wiring in the editor; `sygnal` itself is unchanged (6.3.0).
50
+
51
+ ### Added
52
+
53
+ - **`sygnal-check lsp` highlights the wiring** as semantic tokens (`textDocument/semanticTokens/full`, custom types `sygnalAction`, `sygnalEvent`, `sygnalSelector`, `sygnalTarget`, `sygnalChild`): action names wherever they're wired (`unhandled` when no model entry handles an intent action), EVENTS types, intent selectors (`untargeted` when nothing the component or a child renders can match, `unverified` when only a dynamic class name in the view might), the view classes / ids / controls an intent of their scope selects, and child components selected with `CHILD.select` (the call, `<Child>` and `<Collection of={Child}>` in the parent's view, the child's name). Only names the checker resolved get a token. `workspace/semanticTokens/refresh` after a check that changed them; the setting `sygnal.highlights` turns them off. A client that keeps one semantic-token provider per file can opt out of the provider (`initializationOptions.semanticTokens: false`) and draw the decoded tokens of `sygnal/highlights` itself. Neovim layers them over its TypeScript server's ([highlight groups](https://sygnal.js.org/integration/editors/#neovim)).
54
+ - **VS Code extension: highlights**, drawn as decorations over the theme's colors (VS Code asks only one semantic-token provider per file, so TypeScript's semantic highlighting stays): bold tinted action names, italic EVENTS types, underlined selectors (amber dashed when unverified, red struck through when nothing renders them), tinted targets, underlined selected children. Theme colors `sygnal.highlight.*`, setting `sygnal.highlights` (unset: on).
55
+
56
+ ## 6.3.0 — 2026-10-11
57
+
58
+ Composing UI with a model (PLAN-9). An app declares its parts once in a catalog, the user describes a page or an edit in words, and a model builds it from those parts: a decision model answering multiple-choice questions (TypeSafe Jev, Ollama's `nimble`) or an LLM with structured output, with the same catalog, app code and tests for both. The composition is plain JSON in state, each part is an ordinary child component, and follow-up requests edit the page while keeping the state of the parts they don't touch. The same catalog lets a chat assistant ask the user with a card instead of in text (`chat({ ask })`), on top of client tools for the `chat` behavior. Around it: `decide()` splits large question sets, `byId()` lenses, and a component's `key` is now its identity.
59
+
60
+ Everything is additive, and `sygnal/ai` stays tree-shaken from apps that don't import it. The lowercase marker tags (`<collection>`, `<switchable>`, ...) are deprecated (they keep working in 6.x; `sygnal-check --fix` migrates them).
61
+
62
+ **Measured impact.**
63
+ - Live accuracy (E-3, `npm run test:compose-live`, every PLAN-9 gate passing): Jev structure 61–65/70, follow-up edits 22/22 tuned and 15/16 held out, keys 100%, about 0.35 s per call; `qwen3:8b` with a schema 11/11 flat dashboards and 15/15 dashboards, 0 invalid; `nimble` with `profile: 'small'` structure 29/70 with 0 invalid layouts (was 5 before the `decide()` split, G-757).
64
+ - Size: the kanban gate went down, (a) 42,504 B with `nativeGlobalThis: false` (was 42,690 B; budget 42,700 B) and (b) 38,485 B by default (was 38,679 B): the lazy marker handlers (G-751) save 223 B, `key` identity and the DOM move fixes add 28 B. `sygnal/ai` is tree-shaken from apps that don't import it; `<Composed>` adds about 0.5 KB gzipped over `catalog.render()` when used, and the fetch driver's `batch` about 0.3 KB.
65
+
66
+ **Companion packages.**
67
+ - **`sygnal-check` 0.6.0:** catalog rules (SYG161, SYG162, SYG250, SYG450 and SYG680 where a literal `defineCatalog({...})` shows them; new SYG253 misspelled declaration keys with a rename fix, SYG254 description lint), the `chat` / `compose` behaviors' keys and actions, and SYG426 for the deprecated lowercase marker tags with `--fix`. Its caret range doesn't reach 0.6.0 from `^0.5.0`, so update the dev dependency.
68
+ - **`create-sygnal-app` 2.4.0:** every template depends on `sygnal` ^6.3.0 and `sygnal-check` ^0.6.0.
69
+ - **VS Code extension 0.3.0:** bundles `sygnal-check` 0.6.0 (the new rules and quick fixes in the editor). The extension itself doesn't change.
70
+ - **Claude Code plugin 0.1.2:** the updated `sygnal-dev` skill (composed UI and chat cards, `key` identity).
71
+
72
+ ### Added
73
+
74
+ - **Large question sets in `decide()`.** A set over `maxQuestions` (default 64, Ollama's limit) or an estimated `maxPromptTokens` goes as parallel requests whose replies arrive as one (`answers` merged, `usage` summed), and a request a server rejects as too large (HTTP 400/413 naming tokens, fields or a limit) is halved and sent again, at most 3 deep; `split: false` turns both off. The fetch driver does it for any request through the new `batch`, `combine` and `split` keys; `renderComponent`'s fake answers such a request as one. Fixes `compose.decisions({ profile: 'small' })` on Ollama, whose layout step for 6–7 elements got HTTP 400 (G-757). [Large question sets](https://sygnal.js.org/guide/ai-decisions/#large-question-sets)
75
+ - **Composing UI with a model** (`sygnal/ai`, PLAN-9). The user describes a page in words, and a model builds it from parts the app prepared: it picks parts, their params and their places, and never writes markup, state paths or action names. `defineCatalog({ parts, containers, guidance, limits, examples })` declares the parts once for both kinds of model: each part is a Sygnal component (or a `render` view function) with a `description` (all a model sees of it), `params` (options, numbers with `bins`, LLM-only `text` / `list`, and records: `from: (state) => records`, one node per record), `state` (a key, a lens, or `(node) => lens`) or `local` state, and placement fields (`in`, `group`, `maxUses`, `required`, `pin`, `companions`). Containers are view functions with named slots (tabs, sections; a built-in `row`), never components, and the model never chooses one. The `compose(catalog, { backend, form, prompt })` behavior keeps the composition as plain JSON in the host's state (`status`, `layout`, `preview`, `decisions`, `error`) with the actions `COMPOSE`, `SET_LAYOUT`, `REMOVE`, `CONTROL` and `DONE`; `catalog.render(state.page, state)` renders it in the host's view, each part a child of the host keyed by its identity, so lenses, `CHILD.select`, `EVENTS`, context and drivers work as for any child. A follow-up `COMPOSE` edits the page (remove, change a setting, add, move, swap) and keeps the instances and state of the parts it doesn't touch; `{ prompt, fresh: true }` starts over. Guide: [Composing UI with a Model](https://sygnal.js.org/guide/ai-compose/), with live demos.
76
+ - **Two backends, one catalog.** `compose.decisions({ model })` asks a decision model (TypeSafe Jev, Ollama's `nimble`) multiple-choice questions through `decide()` on the `HTTP` sink, in at most two calls, posting to `/api/decide` by default so the API key stays on your server (options `url`, `rowMean`, `prefilter` (`'auto'` by default: on for large catalogs with categories), `maxOptions`, `maxParts`, `profile: 'small'` for small local models, and `maxQuestions` / `maxPromptTokens` / `split` for `decide()`). `compose.llm()` sends one structured-output request with the catalog's JSON Schema on the `LLM` sink, streamed as a preview, with a repair step; a reply still invalid after it is an error, never a layout. Measured on the research catalogs: Jev 9/11 flat dashboards, 191/210 structure cases and 57/60 enum params at about 0.35 s; `qwen3:8b` with a schema 11/11 at about 3.4 s (the guide's table). Switching backends changes no app code and no test.
77
+ - **`<Composed catalog={catalog} from="page" />`**: `catalog.render()` as a tag (from `sygnal/ai`, also `sygnal`), reading the composition from the host's state at a key or a path; a container view that throws renders the host's `onError` (SYG408). An app that never renders it ships none of it (about 0.5 KB gzipped when used).
78
+ - **Composing from an agent**: `actions: { ...compose.action('page') }` on an `agent` declaration offers `<agent>_page_compose({ prompt, fresh? })`, which answers once the composition is done; `compose.summary(slice)` is the page as an agent's `read` shows it.
79
+ - **Cards in a chat: `chat({ ask: { catalog } })`.** The model gets an `ask_user({ need })` tool; the app composes a card from the catalog (`compose.llm()` on the chat's sink by default, or `compose.decisions()`; `direct: true` takes the card as the tool input for providers that enforce tool schemas), keeps it on the tool part as `part.ui` (status, layout, answer, local state, the backend's decisions; persisted with the chat), and `askCard(catalog, state, part)` renders it. A part with `result: true` answers with `compose.answer(props, value)` to `PARENT`; the call closes with `{ part, answer }` and the chat goes on. STOP and a new message cancel open cards; a card that fails closes the call with the reason, so the model can ask in text. Guide: [Cards in a Chat](https://sygnal.js.org/guide/ai-cards/), with a live demo.
80
+ - **Client tools on the `chat` behavior**: `clientTools: { name: { description, inputSchema } }` declares tools the app answers itself (the AI SDK's `onToolCall` / `addToolOutput`): their calls stay open until `'assistant.TOOL_OUTPUT'` (`{ toolCallId, output }` or `{ toolCallId, errorText }`) answers them, a host entry `'assistant.TOOL_CALL'` runs as each opens, and `openToolCalls(state.assistant)` lists them. A Standard Schema input is validated first. The chat driver's `abort` takes a list of keys, and a chat request with `output` and `parse: false` sends the schema but leaves the reply's `text` for the caller to parse (`compose.llm()` parses leniently).
81
+ - **`chat({ model: (state) => id })`** and `compose.llm({ model: (state) => id })`: the model as a function of the host's state, resolved per request, so one chat behavior follows an app's model picker.
82
+ - **`composeWith(t, catalog, tree)`** for tests: it answers the pending composition, a page or a card, so that it becomes `tree`, through the pending backend's own fake reply, so one test runs against both backends; it throws when the backend can't reach the tree. Also `fromTree`, `toTree` and `removeNode` for layouts.
83
+ - **`byId(field, id)`** (from `sygnal`): a `{ get, set }` lens to the array element with that id, for a catalog's record parts or any child (`<TodoItem state={byId('todos', 3)} />`).
84
+ - **A component's identity is its `key`** (PLAN-9 D470): a component tag's instance belongs to the component plus its `key`, else its `id` prop, else its position in the view (before, `key` was ignored on component tags). A new key disposes the instance and starts a new one, which is how to reset a child; a `key` on `<Collection>` re-creates its items. New **SYG453** (dev only): two tags of one component with the same identity in one view share one instance. +6 B gzipped.
85
+ - **New diagnostic codes** for catalogs, compositions and cards, at run time in development and, where a literal `defineCatalog({...})` shows them, in `sygnal-check`: SYG160 (a result part outside a card), SYG161 (a container that is a component), SYG162 (a part with neither `component` nor `render`), SYG250–SYG252 (unknown part or slot, invalid param value, a record `from()` that returns no `{ value }` array), SYG450–SYG452 (an `in` naming an unknown slot, `<Composed>` misuse, a node listed twice), SYG680–SYG683 (an LLM-only part under a decision backend, a backend option problem, an answer outside the criteria, an LLM layout invalid after repair), SYG684–SYG686 (a client tool shadowing an agent tool, an answer for a call that isn't open, an `ask` catalog without a result part).
86
+ - **`sygnal-check`: catalog rules.** New **SYG253** (warn: a misspelled catalog declaration key, with a rename quick fix) and **SYG254** (info: a description lint: two parts outside one group sharing a phrase, a description naming its own param's value, options that overlap between a part's params); the `chat` and `compose` behaviors' keys and actions are known to the checker (no false SYG127 for `ask`). New **SYG426** for the deprecated lowercase marker tags (below), with `--fix`.
87
+ - **Agent context**: `llms.txt` (now up to 322 lines, D461) and the `sygnal-dev` skill describe catalogs, `compose`, `<Composed>`, `composeWith`, chat cards, client tools and `key` identity; `dist/guide` ships `ai-compose.md` and `ai-cards.md`. Examples: `examples/ai-compose` (both backends, a model picker, an optional dev proxy for Jev that keeps the key in an env file outside the repo) and `examples/ai-ask` (an assistant that asks with cards).
88
+
89
+ ### Changed
90
+
91
+ - **`examples/ai-panel-spa` runs on `sygnal/ai`** against a local Ollama: `makeChatDriver` over `openResponses()` with structured output and streamed turns (it had a hand-written driver with an API key in the browser), model pickers read from Ollama's installed models, a decision model that picks each speaker through `decide()`, `persist()` and the browser driver's clipboard instead of custom drivers, and canonical forms throughout. No library change.
92
+
93
+ ### Deprecated
94
+
95
+ - **The lowercase marker tags** (PLAN-9 D471): `<collection>` and `<switchable>`, which worked without an import, and the undocumented `<clientonly>`, `<portal>`, `<transition>` and `<suspense>`. They keep working in 6.x and will be removed in the next major, where they render unknown elements; the Collection and Switchable handlers will then load only in apps that import them (measured on a prepared patch: about 2.9 KB gzipped less for an app that uses neither, about 0.7 KB for kanban). To migrate, `import { Collection } from 'sygnal'` and write `<Collection …>` (`Switchable`, `Portal`, `Transition`, `Suspense` from `'sygnal'`, `ClientOnly` from `'sygnal/vike'`); `sygnal-check --fix` renames the tags and adds the import. New code SYG426: the dev diagnostics warn once per tag for `<collection>` / `<switchable>` (0 production bytes), `sygnal-check` reports every lowercase marker tag, and the JSX types mark the two intrinsic elements `@deprecated`. A plain `<slot>` is an HTML element, as before (G-480). `examples/todomvc` uses `<Collection>`.
96
+
97
+ ### Fixed
98
+
99
+ - **Real DOM: a keyed component that moves to another parent element in one patch** (G-750, G-753). Moving an instance to an earlier parent element threw `NotFoundError` in the DOM driver and stopped the app's DOM updates, and moving one into a parent element created in the same patch (a new row) left the DOM at the old arrangement, silently. Both are fixed in the DOM driver's isolation module; the mock DOM of `renderComponent` never showed them. Browser tests on Chromium, Firefox and WebKit.
100
+ - **The `chat` behavior after a reload.** A chat slice persisted while a reply streamed came back with `status: 'streaming'`, and Send did nothing until the page was cleared, in every app that persisted a chat. It now comes back `'ready'`, and tool calls left open are closed.
101
+ - **`Portal`, `Transition`, `Suspense` and `lazy` are tree-shaken** (G-751): they were registered when the main entry loaded, so every app carried them; they now register when first rendered. Kanban is 223 B gzipped smaller.
102
+
103
+ ## sygnal-check 0.5.1, VS Code extension 0.2.1 — 2026-10-11
104
+
105
+ A patch for the editor tooling; `sygnal` itself is unchanged (6.2.0). Reported against extension 0.2.0: opening `examples/todomvc` (code at the top level, no `src/`), or the repository root, underlined nothing and said nothing.
106
+
107
+ ### Fixed
108
+
109
+ - **`sygnal-check lsp` finds nested projects.** Each file is checked as part of its project root: the nearest folder, up to its workspace folder, with a `sygnal-check.config.json` or a `package.json` that depends on `sygnal` / `sygnal-check` (else the workspace folder, as before). Each root has its own config, `paths` (relative to the root), cache and settings (`workspace/configuration` per root). Opening a repository or monorepo root checks each app as its own project, from the first file you open in it; a workspace folder that only holds projects below it isn't checked as one.
110
+ - **`sygnal-check lsp` says when a file isn't checked.** A one-time message when an open Sygnal file is outside its project's `paths`, or when `paths` match no source files (`window/showMessage`, and the log).
111
+ - **VS Code extension:** it starts in larger monorepos (it looks through up to 500 `package.json` files for one that uses Sygnal, was 50).
112
+
113
+ ### Changed
114
+
115
+ - **Every app in `examples/` has a `sygnal-check.config.json`** (`strict: true`, as the templates; `todomvc` adds `paths: ["."]`, `vike` `paths: ["pages"]`), so the CLI, the dev overlay and the editor check them by default.
116
+
5
117
  ## 6.2.0 — 2026-10-10
6
118
 
7
119
  Two things since 6.1.1. Agents get read-only tools that take an input (`agent.queries`) and actions that answer once their result is in state (`until`). And the editor tooling (PLAN-7 Phase 4) grows from findings to navigation: the language server in `sygnal-check` 0.5.0 adds completions, go to definition / references, code lenses and rename for the names Sygnal wires by string, in VS Code (extension 0.2.0), Neovim, Helix, JetBrains IDEs and any other LSP editor.
package/README.md CHANGED
@@ -515,7 +515,9 @@ h('div', [h('h1', 'Hello'), h('button.btn', 'Click')])
515
515
  | [Vike SSR](./examples/vike) | File-based routing with SSR, layouts, and data fetching |
516
516
  | [Advanced Features](./examples/advanced-feature-tests) | Portals, slots, disposal, suspense, lazy loading |
517
517
  | [TypeScript 2048](./examples/ts-example-2048) | Full game in TypeScript |
518
- | [AI Discussion Panel](./examples/ai-panel-spa) | Complex SPA with custom drivers |
518
+ | [AI Discussion Panel](./examples/ai-panel-spa) | `sygnal/ai` on a local Ollama: streaming chat, structured output, a decision model picking speakers |
519
+ | [Compose a Workspace](./examples/ai-compose) | A decision model (`decide()`, local Ollama) picks and arranges prepared components from a prompt: the json-render + Jev idea |
520
+ | [Ask with Cards](./examples/ai-ask) | A chat assistant on a local Ollama that asks with cards built from prepared parts (`chat({ ask })`) and continues with the answer |
519
521
  | [Sygnal ToDoMVC](https://github.com/tpresley/sygnal-todomvc) | [Live Demo](https://tpresley.github.io/sygnal-todomvc/) |
520
522
  | [Sygnal 2048](https://github.com/tpresley/sygnal-2048) | [Live Demo](https://tpresley.github.io/sygnal-2048/) |
521
523
  | [Sygnal Mahjong](https://github.com/tpresley/mahjong-trainer) | [Live Demo](https://tpresley.github.io/mahjong-trainer/) |
package/dist/ai.cjs.js CHANGED
@@ -4,6 +4,10 @@ var sygnal = require('sygnal');
4
4
 
5
5
 
6
6
 
7
+ Object.defineProperty(exports, 'Composed', {
8
+ enumerable: true,
9
+ get: function () { return sygnal.Composed; }
10
+ });
7
11
  Object.defineProperty(exports, 'agentTools', {
8
12
  enumerable: true,
9
13
  get: function () { return sygnal.agentTools; }
@@ -20,6 +24,10 @@ Object.defineProperty(exports, 'anthropicMessages', {
20
24
  enumerable: true,
21
25
  get: function () { return sygnal.anthropicMessages; }
22
26
  });
27
+ Object.defineProperty(exports, 'askCard', {
28
+ enumerable: true,
29
+ get: function () { return sygnal.askCard; }
30
+ });
23
31
  Object.defineProperty(exports, 'chat', {
24
32
  enumerable: true,
25
33
  get: function () { return sygnal.chat; }
@@ -40,10 +48,22 @@ Object.defineProperty(exports, 'commandBar', {
40
48
  enumerable: true,
41
49
  get: function () { return sygnal.commandBar; }
42
50
  });
51
+ Object.defineProperty(exports, 'compose', {
52
+ enumerable: true,
53
+ get: function () { return sygnal.compose; }
54
+ });
55
+ Object.defineProperty(exports, 'composeWith', {
56
+ enumerable: true,
57
+ get: function () { return sygnal.composeWith; }
58
+ });
43
59
  Object.defineProperty(exports, 'decide', {
44
60
  enumerable: true,
45
61
  get: function () { return sygnal.decide; }
46
62
  });
63
+ Object.defineProperty(exports, 'defineCatalog', {
64
+ enumerable: true,
65
+ get: function () { return sygnal.defineCatalog; }
66
+ });
47
67
  Object.defineProperty(exports, 'encodeOpenResponses', {
48
68
  enumerable: true,
49
69
  get: function () { return sygnal.encodeOpenResponses; }
@@ -60,6 +80,10 @@ Object.defineProperty(exports, 'fromAISDK', {
60
80
  enumerable: true,
61
81
  get: function () { return sygnal.fromAISDK; }
62
82
  });
83
+ Object.defineProperty(exports, 'fromTree', {
84
+ enumerable: true,
85
+ get: function () { return sygnal.fromTree; }
86
+ });
63
87
  Object.defineProperty(exports, 'jsonSchema', {
64
88
  enumerable: true,
65
89
  get: function () { return sygnal.jsonSchema; }
@@ -84,6 +108,10 @@ Object.defineProperty(exports, 'openResponses', {
84
108
  enumerable: true,
85
109
  get: function () { return sygnal.openResponses; }
86
110
  });
111
+ Object.defineProperty(exports, 'openToolCalls', {
112
+ enumerable: true,
113
+ get: function () { return sygnal.openToolCalls; }
114
+ });
87
115
  Object.defineProperty(exports, 'outputJsonSchema', {
88
116
  enumerable: true,
89
117
  get: function () { return sygnal.outputJsonSchema; }
@@ -92,6 +120,10 @@ Object.defineProperty(exports, 'parseInput', {
92
120
  enumerable: true,
93
121
  get: function () { return sygnal.parseInput; }
94
122
  });
123
+ Object.defineProperty(exports, 'removeNode', {
124
+ enumerable: true,
125
+ get: function () { return sygnal.removeNode; }
126
+ });
95
127
  Object.defineProperty(exports, 'score', {
96
128
  enumerable: true,
97
129
  get: function () { return sygnal.score; }
@@ -104,6 +136,10 @@ Object.defineProperty(exports, 'toJsonSchema', {
104
136
  enumerable: true,
105
137
  get: function () { return sygnal.toJsonSchema; }
106
138
  });
139
+ Object.defineProperty(exports, 'toTree', {
140
+ enumerable: true,
141
+ get: function () { return sygnal.toTree; }
142
+ });
107
143
  Object.defineProperty(exports, 'uiMessageStream', {
108
144
  enumerable: true,
109
145
  get: function () { return sygnal.uiMessageStream; }
@@ -1 +1 @@
1
- {"version":3,"file":"ai.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"ai.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/ai.esm.js CHANGED
@@ -1,2 +1,2 @@
1
- export { agentTools, agui, answers, anthropicMessages, chat, chatCompletions, choice, chromePrompt, commandBar, decide, encodeOpenResponses, experimentalExposeWebMcp, formTool, fromAISDK, jsonSchema, makeChatDriver, makeMcpAppDriver, messageText, noul, openResponses, outputJsonSchema, parseInput, score, strictSchemas, toJsonSchema, uiMessageStream, withToolResults } from 'sygnal';
1
+ export { Composed, agentTools, agui, answers, anthropicMessages, askCard, chat, chatCompletions, choice, chromePrompt, commandBar, compose, composeWith, decide, defineCatalog, encodeOpenResponses, experimentalExposeWebMcp, formTool, fromAISDK, fromTree, jsonSchema, makeChatDriver, makeMcpAppDriver, messageText, noul, openResponses, openToolCalls, outputJsonSchema, parseInput, removeNode, score, strictSchemas, toJsonSchema, toTree, uiMessageStream, withToolResults } from 'sygnal';
2
2
  //# sourceMappingURL=ai.esm.js.map
@@ -742,6 +742,12 @@ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${
742
742
  * /__sygnal/mcp (./mcp.ts: streamable HTTP, loopback / local Host / local Origin only)
743
743
  * and the dev entries also import 'virtual:sygnal/mcp', which connects the page over the
744
744
  * HMR channel (installMcpBridge from 'sygnal/devtools'). Never in `vite build` or Vitest.
745
+ * 11. SYG103 view words (dev server with diagnostics on; never `vite build`, SSR or Vitest): a
746
+ * module that sets `X.intent` or `X.uses` gets a tail registering, for each such X, the
747
+ * words of its string literals and of the relative modules it imports, in
748
+ * `globalThis.__SYGNAL_VIEW_TOKENS__`. The runtime SYG103 check then takes a selector drawn
749
+ * by a view helper (`{state.open && panel()}`) as conditionally rendered, as it does for
750
+ * markup in the view function itself (see viewTokensTail).
745
751
  *
746
752
  * Why not Vite's `define`? Vite's dependency optimizer does not apply user
747
753
  * `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
@@ -979,9 +985,20 @@ function sygnal(options = {}) {
979
985
  transform(code, id, opts) {
980
986
  // SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
981
987
  const adapters = adapterImports(this, code, id);
982
- if (adapters)
983
- return checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts));
984
- return devTransform.call(this, code, id, opts);
988
+ const result = adapters
989
+ ? checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts))
990
+ : devTransform.call(this, code, id, opts);
991
+ // 11. the words SYG103 may take as conditionally rendered markup (dev server, diagnostics on)
992
+ const tokens = isServe && !isVitest && devOn && !opts?.ssr ? viewTokensTail(this, code, id) : null;
993
+ if (!tokens)
994
+ return result;
995
+ return Promise.all([result, tokens]).then(([r, tail]) => {
996
+ if (!tail)
997
+ return r;
998
+ if (r == null)
999
+ return withSourcemap(code, [], tail, id);
1000
+ return typeof r == 'string' ? r + tail : { ...r, code: r.code + tail };
1001
+ });
985
1002
  },
986
1003
  };
987
1004
  function devTransform(code, id, opts) {
@@ -1064,6 +1081,55 @@ function sygnal(options = {}) {
1064
1081
  return done();
1065
1082
  }
1066
1083
  }
1084
+ // 11. SYG103 (the dev runtime check of an intent selector that never matched) lets a selector
1085
+ // through as conditionally rendered when its words appear in the component's view; the runtime
1086
+ // can read only the view function's own source. A component module (one that sets `X.intent` or
1087
+ // `X.uses`) gets, in dev, a tail registering for each such component the words of its string
1088
+ // literals and those of the relative modules it imports, where view helpers live
1089
+ // (globalThis.__SYGNAL_VIEW_TOKENS__, a WeakMap read by checks/dom.ts).
1090
+ const COMPONENT_WITH_SELECTORS_RE = /(?:^|[^\w$.])([A-Z][\w$]*)\.(?:intent|uses)\s*=(?!=)/g;
1091
+ const STRING_RE = /(['"])((?:\\.|(?!\1)[^\\\n])*)\1|`((?:\\[\s\S]|[^\\`])*)`/g;
1092
+ const RELATIVE_IMPORT_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"])(\.{1,2}\/[^'"]+)\1/g;
1093
+ // A string that is itself a selector ('.save', '#id', 'form.ask', '[data-x]') or a path / URL: its
1094
+ // words aren't markup (the intent's own DOM.click('.svae') would otherwise vouch for its typo)
1095
+ const NOT_MARKUP_RE = /(?:^|[\s>+~,(])[\w-]*[.#[][\w-]|^\s*(?:\/|[\w+-]+:\/\/)/;
1096
+ /** the class / id / tag-like words in a module's string and template literals (comments skipped) */
1097
+ function stringWords(code, into) {
1098
+ for (const m of blankOut(code, false).matchAll(STRING_RE)) {
1099
+ const text = m[2] ?? m[3] ?? '';
1100
+ if (NOT_MARKUP_RE.test(text))
1101
+ continue;
1102
+ for (const w of text.match(/[A-Za-z_][\w-]*/g) || [])
1103
+ into.add(w);
1104
+ }
1105
+ }
1106
+ function viewTokensTail(ctx, code, id) {
1107
+ const file = cleanId(id);
1108
+ if (!/\.[jt]sx?$/.test(file) || id.includes('node_modules') || id.startsWith('\0'))
1109
+ return null;
1110
+ const names = new Set();
1111
+ for (const m of blankOut(code, true).matchAll(COMPONENT_WITH_SELECTORS_RE))
1112
+ names.add(m[1]);
1113
+ if (!names.size)
1114
+ return null;
1115
+ const words = new Set();
1116
+ stringWords(code, words);
1117
+ const specs = new Set(Array.from(blankOut(code, false).matchAll(RELATIVE_IMPORT_RE), m => m[2]));
1118
+ return Promise.all([...specs].map(async (spec) => {
1119
+ try {
1120
+ const r = await ctx.resolve(spec, id);
1121
+ const dep = r && !r.external && cleanId(r.id);
1122
+ if (!dep || !/\.[cm]?[jt]sx?$/.test(dep) || dep.includes('node_modules'))
1123
+ return;
1124
+ ctx.addWatchFile?.(dep);
1125
+ stringWords(await fs.promises.readFile(dep, 'utf8'), words);
1126
+ }
1127
+ catch (_) { /* unreadable: its words are just not added */ }
1128
+ })).then(() => {
1129
+ const comps = [...names].map(n => `typeof ${n} == 'function' && ${n}`).join(', ');
1130
+ return `\n;(() => { try { const m = globalThis.__SYGNAL_VIEW_TOKENS__ ||= new WeakMap(), t = ${JSON.stringify([...words].join(' '))}; for (const c of [${comps}]) if (c) m.set(c, t) } catch (_) {} })();\n`;
1131
+ });
1132
+ }
1067
1133
  // PLAN-5 W-2 (D209): the adapter entries and the optional peer dependencies each one imports
1068
1134
  // (the Zag packages pinned together: they depend on each other's exact versions)
1069
1135
  const ZAG = '~1.45.0';